log in
consulting hosting industries the daily tools about contact

PostGraphile: The Schema You'll Regret in 18 Months

PostGraphile generates a GraphQL API from your Postgres schema in minutes. That speed comes with a tax you don't notice until you're deep in production.

PostGraphile will have you running a full GraphQL API against your Postgres database in under an hour. That is genuinely impressive and also, in my experience, the start of a slow-motion problem you won't fully see until your client is asking why a mobile app query takes 4 seconds.

I've now used PostGraphile on three separate projects — one e-commerce platform, one biotech internal tool, and one real estate data product — and every time the first week felt like magic and the eighteenth month felt like archaeology. Here's what I wish someone had told me.

What PostGraphile Actually Does

You point PostGraphile at a Postgres database and it reflects your schema — tables, views, functions, relationships, constraints — and emits a GraphQL API. Foreign keys become connections. Stored procedures become mutations or queries. Row-level security policies get honored if you wire up the JWT integration. It's not a code generator; it's a live introspection layer that rebuilds on schema changes.

For internal tooling or rapid prototyping, that's a genuine superpower. I stood up a read-heavy API for a biotech client last year — exposing assay results, sample metadata, joining across maybe a dozen tables — and I had a working, filterable, paginated GraphQL endpoint in a day. Their data scientists were querying it from Jupyter notebooks by end of week. If I'd hand-rolled that in Laravel, I'd have been writing QueryBuilder boilerplate for two weeks.

That's the real pitch: PostGraphile collapses the distance between your data model and your API surface. When your data model is stable and your consumers are internal and trusted, that's a legitimate win.

The Schema It Generates

Here's a simplified version of what PostGraphile emits for a table like this:

CREATE TABLE samples (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  accession_number TEXT NOT NULL,
  patient_id UUID REFERENCES patients(id),
  collected_at TIMESTAMPTZ,
  status TEXT,
  metadata JSONB
);

You get, automatically:

  • allSamples — a paginated connection with Cursor-based pagination
  • sampleById — node lookup
  • createSample, updateSampleById, deleteSampleById — mutations
  • Filtering via a generated SampleCondition input type
  • Ordering via SamplesOrderBy enum

And if you have a stored function:

CREATE FUNCTION samples_with_failed_qc()
RETURNS SETOF samples
LANGUAGE sql STABLE
AS $$
  SELECT * FROM samples WHERE metadata->>'qc_status' = 'failed';
$$;

That becomes a samplesWithFailedQc query automatically. No resolver code. No schema definition. It just appears.

For the first few months, this feels like cheating in the good way.

Where It Starts to Hurt

Your database schema is now your public API contract

This is the footgun. When PostGraphile reflects your schema directly, renaming a column is a breaking API change. Adding a NOT NULL constraint changes what your mutations accept. Splitting a table for normalization reasons reshapes your GraphQL types in ways that break every client query referencing that type.

On the biotech project I mentioned, we ended up in a situation where the database team wanted to rename collected_at to collection_timestamp for consistency with a new naming standard. Completely reasonable. Except that field name was baked into a dozen saved queries in the client's Jupyter notebooks, their internal dashboard, and a PDF report generator. A column rename became a multi-team coordination event.

With a hand-rolled API, you add a resolver alias and move on. With PostGraphile, you're either reaching for @rename smart comments — which work but feel like duct tape — or you're locking your database naming conventions to what you shipped on day one.

-- Smart comment workaround -- functional but now your DB has API concerns in it
COMMENT ON COLUMN samples.collection_timestamp IS E'@name collectedAt';

That works. But now your database schema has presentation-layer concerns embedded in SQL comments. That's a smell.

The N+1 problem is real and non-obvious

PostGraphile uses Graphile's look-ahead and DataLoader internally, so it's not naive. But the moment you start nesting connections — samples with their patients, patients with their orders, orders with line items — you can construct queries that look innocuous in GraphQL and generate genuinely punishing SQL. I've watched a single nested query from a frontend developer produce a 14-table join with no warning.

You don't have the usual escape valve of "write a custom resolver that does this efficiently." You're working within what PostGraphile generates, and tuning it requires either Postgres query optimization (legitimate) or bending toward stored functions for everything expensive (which defeats some of the ergonomic benefit).

Authorization is a Postgres problem now

PostGraphile's RLS integration is elegant in theory. You set up Postgres roles, write row-level security policies, pass JWTs through, and Postgres enforces access control at the data layer.

In practice, I find this creates organizational friction. Most teams I work with think about authorization in application code. Their developers understand Laravel policies and middleware. They do not understand CREATE POLICY and SET LOCAL role. When something goes wrong — and it will go wrong — you're debugging auth failures in the database layer, not the application layer, and that's a different skill set.

On the real estate project, a misconfigured RLS policy silently returned empty result sets instead of erroring. We spent two days thinking it was a query bug before we looked at the Postgres role context.

Your schema grows in one direction: outward

Because everything is auto-generated, there's no forcing function to prune or version your API. You add a table, you get more queries and mutations. You never remove anything because removal is a breaking change. Eighteen months in, you have a schema with 200 types and your client's frontend team is using graphql-codegen to generate TypeScript types and the output file is 8,000 lines.

That's not PostGraphile's fault exactly, but hand-rolled APIs have natural checkpoints — you have to write the resolver, you have to add it to the schema — that create friction against unbounded growth. PostGraphile removes that friction entirely.

When I'd Reach for It

Internal tooling with a stable schema. If you're building a dashboard for internal users, the data model isn't changing constantly, and the consumers are trusted, PostGraphile is genuinely great. The productivity gain is real.

Rapid prototyping before you know your query patterns. Let PostGraphile run for the first few months while you figure out what your clients actually query. Then you have data to inform a hand-rolled API design.

Read-heavy APIs with controlled consumers. PostGraphile's generated filtering and pagination is better than what most people write by hand. If you're mostly doing reads and you control the clients, you can manage the schema coupling risk.

When I Wouldn't

Public APIs. You cannot expose PostGraphile's auto-generated schema to the public internet and expect to maintain it. The coupling between your data model and your API contract will eventually create a situation where normal database maintenance work breaks external clients.

Multi-tenant SaaS products. Authorization complexity gets unwieldy fast. I'd rather write explicit Laravel policies I can audit and test than debug RLS policy interactions under load.

Teams where the DB and API teams are different people. This sounds like it would be fine — they can coordinate — but in practice, the coupling creates constant low-grade friction. The database team will eventually want to do something that the API team can't absorb quickly.

Anything with complex business logic in mutations. PostGraphile mutations are basically direct table operations with optional stored function hooks. The moment your "create order" mutation needs to check inventory, fire a webhook, update a ledger, and send an email, you're writing a stored procedure to hold all that logic and now your business logic lives in Postgres. That's a maintainability risk I don't like.

The Honest Summary

I don't regret using PostGraphile on the projects where I used it. The biotech project especially — it shipped fast, the data scientists got what they needed, and for a read-heavy internal tool that's still running two years later, it was the right call.

But I've been careful since then. PostGraphile is an API that optimizes for the first sprint at the cost of the tenth. That's a legitimate tradeoff if you go in with eyes open, budget for the abstraction leaks, and know that at some point you'll probably want to peel it back and replace hot paths with explicit resolvers or a more conventional API layer.

The mistake is treating it as a permanent production API foundation for a system that's going to grow. The auto-generated schema is beautiful on day one. By month eighteen, beauty is not the word you'll use.

Need help shipping something like this? Get in touch.