Prisma and PostgreSQL as the Product Source of Truth

I do not think of PostgreSQL as only infrastructure. In a product app, it is where the product remembers what happened.

That makes database design a product decision. If a user creates a request, the system should know who created it, what state it is in, when it changed, and what later workflows are allowed to do with it. The UI can display that state, but it should not be the source of truth.

Prisma helps because it makes the data model visible in code and gives typed access to the database. A small create operation can be easy to read:

export async function createRequest(input: CreateRequestInput) {
  return db.request.create({
    data: {
      title: input.title,
      status: "draft",
      createdById: input.userId,
    },
  });
}

This is a simplified example, not a real product schema. The important part is the boundary. A later workflow should read the durable state from the database instead of guessing from a page, cache, or client-side object.

The place where I am careful with Prisma is reach. A typed client is convenient enough that it can leak everywhere. If routes, components, background workers, and tests all call Prisma directly, product rules spread out quietly.

For example, a request may only be submitted when it has a title and the user has permission. That rule should not be copied into three route actions. I would rather put it behind a service:

export async function submitRequest(input: SubmitRequestInput) {
  const request = await requestRepo.findForUpdate(input.requestId);

  assertCanSubmit(request, input.userId);

  return requestRepo.updateStatus(request.id, "submitted");
}

The repository can still use Prisma. The service owns the rule.

PostgreSQL also gives useful product tools beyond simple storage. Transactions matter when a workflow changes more than one thing. Constraints matter when bad state should be impossible. Indexes matter when the product grows and the common screens need to stay fast.

The trade-off is that the database becomes a serious part of the design. Migrations need care. Permissions need care. A field that looks harmless today can become part of the public behavior tomorrow. Renaming or deleting it later may be harder than expected.

Prisma does not remove that work. It makes some of it nicer. The schema is easier to review than scattered SQL strings, and generated types catch many simple mistakes. But it can also hide query costs if I stop thinking about the database underneath.

The approach I prefer is boring:

  • PostgreSQL stores durable product state
  • Prisma provides typed access and migrations
  • services hold product rules
  • routes and workers call services, not random database operations
  • tests cover the rules where they live

I would not use this exact shape for every project. If the app needs unusual SQL, heavy analytics, or database features that Prisma does not express well, I would use SQL more directly in those areas. If the app is a small prototype, I might accept more direct Prisma calls until the workflow proves it needs a stronger boundary.

The main point is not Prisma itself. The main point is treating persistent state as part of the product contract. Once users depend on it, it deserves clear ownership.

Related Posts

Astro for Documentation and a Professional Site

I use Astro because this site is mostly writing. I do not need a heavy app framework for pages that should load fast and be easy to edit. That sounds simple, but it is the mai

read more

Localization in Product Apps

Localization is not only replacing English strings with another language. In a product app, language touches workflow. It changes labels, validation messages, dates, empty states, permissions copy, d

read more

MCP as a Safe AI Integration Boundary

MCP is interesting because it makes AI integrations feel less like prompt magic and more like software boundaries. That is the part I care about. A model should no

read more

Zod, OpenAPI, and Swagger for API Contracts

A public API is not just backend code. It is a product surface for another developer. That means the contract has to be readable. It also has to be enforced at runtime. Types in the app are useful, b

read more

pg-boss for Durable Background Jobs

The customer problem was not "we need a queue". The problem was that a slow operation made the user wait with no clear answer. That distinction matters. A queue is an implementation detail. The produ

read more

Pragmatic Drag and Drop for Real Ordering Tasks

Drag and drop is easy to add for a demo and harder to make reliable for real work. The product question is not "can the item move on screen?" The question is whether the user can safely change an ord

read more

React Router for Full-Stack Product Workflows

A route is not only a URL. In a product app, a route often represents a task the user is trying to finish. That sounds obvious, but it changes how I design the code. A settings page that starts an im

read more

shadcn-Style UI as an Owned Product System

I like copied UI primitives because they make the component library feel like part of the app, not something the app is borrowing. That is the part of the shadcn/ui-style ap

read more

Dense Operational UI with Tables and Editors

Sometimes a simple form is the wrong UI. If the user needs to compare many values and make careful edits, a table can be kinder than a long page of inputs. Dense UI has a bad reputation when it is us

read more

Vercel AI SDK with Explicit Tool Boundaries

The risky part of an AI feature is not the chat UI. The risky part is what the chat is allowed to do. It is easy to make an assistant feel powerful by giving it tools. With something like the [Vercel

read more

Vertical Slice Architecture with Dependency-Cruiser

I like vertical slices because they make a feature easier to delete, move, or review. The folder structure is not the main value. The value is that the code for one workflow is not spread across ten u

read more

Testing Product Workflows with Vitest and Playwright

I do not want a test suite that only proves functions work. I want it to protect the workflows that would hurt if they broke. That does not mean every rule needs a browser test. Browser tests are val

read more

Zod Beyond Validation

Zod is usually introduced as a validation library. That is true, but the more useful idea is boundary definition. A TypeScript type only helps after data is already inside the pro

read more