Skip to content

Custom software development

Custom Software Development with Next.js and PostgreSQL

What choosing PostgreSQL changes in a Next.js build - the constraints it lets you enforce, the search you no longer need a service for, and the trap that catches every team once.

Choosing PostgreSQL for a custom build is a decision with specific consequences, and they are worth stating rather than assumed. It is the default on these projects, not a rule, and the reason is that it moves work out of the application and into a place where it cannot be bypassed.

Constraints the application cannot forget

The strongest argument for a relational database in custom software has nothing to do with performance.

An application is written by several people over several years, and not all of them will know the rules. A guarantee enforced in application code holds for the paths somebody remembered - and not for the import script, the console command, the scheduled job, or the admin screen added next spring.

A guarantee enforced in the database holds for all of them. A foreign key means an order cannot reference a customer that does not exist. A unique constraint means two concurrent requests cannot both create the same record, which no amount of checking-then-inserting in application code achieves. A check constraint means a quantity cannot be negative in the row, not merely in the form.

PostgreSQL goes further than most in a way that matters for real applications. An exclusion constraint can refuse a booking whose date range overlaps an existing one for the same resource - a race condition that is genuinely hard to close in application code and trivial to close here. Partial indexes let you index only the rows that matter, so a query over active records does not pay for the archive.

What that removes from the codebase

Each of those constraints is validation code you do not write, do not test and do not have to remember when adding a seventh way to create a record.

It is also the difference between a bug that is impossible and a bug that is unlikely. In a system that handles money, stock or appointments, the distinction is the entire point.

JSONB, and where the line is

Some data genuinely resists a schema. A webhook payload from a third party. A form whose fields the customer configures. Per-tenant settings that differ by tenant.

PostgreSQL handles that honestly: JSONB is stored efficiently, queried with real operators and indexed with GIN, so unstructured data is a first-class choice rather than a workaround.

The line worth holding is that anything you filter, sort, report on, join by or constrain gets a column. The failure pattern is familiar and slow - a JSON column becomes the place new fields go because adding a migration felt heavy, and two years later nobody can answer a question about the data without reading application code.

Search, without a second system

A large share of applications add a search service, a sync pipeline and an operational dependency to do something the database can already do.

Full-text search in PostgreSQL has stemming, ranking and multi-language configurations, and it indexes well. For a few hundred thousand records it is fast, and it has the significant advantage that search results cannot be stale, because there is nothing to synchronise.

Where it runs out is real and worth naming - typo tolerance, large-scale relevance tuning, faceted search over millions of documents. Reaching that point is a measurement, not a prediction, and arriving there later costs far less than operating a search cluster from the first week for a dataset that never needed one.

The one trap, and how it is handled

Every team meets this once. A Next.js application on a serverless runtime opens a database connection per instance, and PostgreSQL will accept a fixed number. Under traffic the application does not get slower - it starts refusing connections, which reads like a database failure and is a configuration mistake.

The answer is a connection pooler between the application and the database, and the part that catches people is that transaction-mode pooling is incompatible with prepared statements unless the client is configured for it. Connection pooling in a serverless runtime is the full explanation.

It is decided in week one on these builds, which is the difference between a known constraint and an incident.

How an engagement runs

In phases, each fixed in writing before it starts and each ending with something deployed that you can use. The first phase takes one workflow end to end rather than the outline of several, because that is what settles the schema against real data.

The database decisions come in that first phase rather than after it. Which constraints are enforced where, what is a column and what is JSONB, how migrations run against a table that will later be large, and what the pooling arrangement is. None of those are expensive to decide early and all of them are expensive to change once there is production data behind them.

What you receive

A deployed application at the end of each phase, the repository with its history intact, and the schema as migrations rather than as a current state - so the database has a history that can be replayed and reviewed.

With it: a written note on the data model and why it is shaped that way, the indexes and what each one is for, the pooling configuration, and the constraints with a line each explaining what they prevent. That last one is the document people actually come back to, because a constraint nobody understands is a constraint somebody eventually removes.

When PostgreSQL is not the answer

If the application is a thin layer over somebody else's API and holds almost no state of its own, a database is a component you are operating for no reason - and a simpler store is the better answer.

If the workload is genuinely a cache or a queue, it should be a cache or a queue. PostgreSQL can do both adequately and neither is what it is for.

And if the data is high-volume append-only measurement - sensor readings, telemetry at scale - a relational table will take a surprising amount of it, but there is a point beyond which a purpose-built store wins. That point should be established by measuring rather than by assuming it has already been reached.

Scope and terms

Engagement model
Fixed scope, agreed in writing before work starts. Not a day rate against an open backlog.
Price and timeline
Both are set per project, once the scope is. Quoted together, before anything is built.
What we need from you
One person who can approve decisions, and access to your repository and issue tracker.
Not included
Anything outside the agreed scope. It becomes its own scope rather than a variation order.
Third-party costs
Hosting, licences, API fees and SaaS subscriptions are contracted and paid by you.
Invoicing
Codefacture Yazılım A.Ş., Türkiye. EUR, USD or GBP by bank transfer, with no Turkish VAT on exported services.

Frequently asked questions

Why PostgreSQL rather than a managed document database?
Because most custom software has relationships, and a database that enforces them is doing work your application would otherwise have to do badly. Foreign keys, unique constraints and check constraints are guarantees that hold regardless of which code path wrote the row - including the console command somebody wrote in a hurry and the import nobody reviewed. A document store asks the application to maintain that discipline forever, and applications outlive the people who understood the discipline.
What is the connection problem everybody warns about?
Each serverless instance that handles a request opens its own database connection, and PostgreSQL has a hard limit on how many it will accept. Under load the application does not slow down, it starts refusing connections - which looks like a database outage and is a configuration mistake. The answer is a pooler in front of the database, and the detail that catches teams is that transaction-mode pooling breaks prepared statements, so the ORM has to be configured for it. It is a solved problem that is extremely unpleasant to discover in production.
Can we keep some data as JSON instead of designing columns for it?
Yes, and PostgreSQL is the database where that is a reasonable decision rather than a retreat. JSONB is indexable, so a field you genuinely cannot model - a third party's payload, a form whose shape the customer controls, settings that differ per tenant - can live there and still be queried. The line worth holding is that anything you filter, sort, report on or join by gets a column. A JSON column that becomes the place new fields go is how a schema stops being a schema.
Does PostgreSQL mean we do not need a search service?
For a great many applications, yes, and that is a service and a sync pipeline you never have to operate. Full-text search with ranking, stemming and multiple languages is built in, and for a few hundred thousand records it is fast enough that the alternative is premature. Where it genuinely runs out is typo tolerance, large-scale relevance tuning and faceted search over millions of documents - and that conclusion should come from a measurement rather than from an assumption.
Is this a different engagement from your other custom software work?
No - it is the same work with the data layer named, which is why this page exists separately. If you arrived looking for a Next.js and PostgreSQL team specifically, this is what that choice changes. The scoping, the phases and the deliverables are the same as on the custom software development page.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page