PostGraphile: Instant GraphQL API from Your Database Schema
AI generated
{ }
type
GraphQL · PostgreSQL · Database Design
PostGraphile: Instant GraphQL API from Your Database Schema
when the schema itself is the single source of truth

PostGraphile reads a PostgreSQL schema and produces a complete, type-safe GraphQL API without writing a single resolver by hand. Tables, foreign keys, constraints and row-level security policies become fields, relationships and permissions directly, making classic boilerplate between database and API practically disappear.

18 min read PostGraphile · PostgreSQL · Row-Level Security · Plugins PostgreSQL 14+ · Node.js

1. What PostGraphile does differently from hand-written GraphQL APIs

Most GraphQL APIs follow the same pattern: a team designs a schema, writes resolvers that load data from a database, and maintains both layers in parallel. PostGraphile flips this relationship. Instead of designing a schema by hand and then writing resolvers, PostGraphile reads an existing PostgreSQL schema and generates a complete GraphQL API from it at runtime. Tables become types, columns become fields, foreign keys become relationships, and constraints become validation rules. The result is an API that exactly mirrors what is actually modeled in the database, with no hand-written resolver logic required.

The practical benefit shows up most clearly with data models that change frequently. When a column is added, an index set, or a new foreign key relationship created in PostgreSQL, the change automatically shows up in the GraphQL schema after a restart. There is no second source of truth that can drift out of sync. For teams already running cleanly modeled PostgreSQL databases, complete with constraints, checks and comments as documentation, PostGraphile is often the fastest path to a production-ready GraphQL API, entirely without the usual boilerplate between ORM, resolver and schema definition.

The approach also has clear limits worth knowing before adoption. PostGraphile works best when the database itself carries the domain logic, through functions, triggers, views and row-level security. Teams that prefer to encapsulate business logic in application code need to partially rethink that approach when adopting PostGraphile. The following sections show how introspection works, how security rules are defined directly in PostgreSQL, and where the limits of an automatically generated API lie.

2. Installation and basic configuration

PostGraphile runs either as a standalone server via the CLI or as middleware inside an existing Node.js application, for example with Express or Fastify. For getting started, the CLI is enough, it expects a PostgreSQL connection string and immediately provides a running GraphQL endpoint with an integrated GraphiQL explorer. From the start, it is important to separate the schema exposed publicly from internal tables only needed internally, PostgreSQL's own schema concept serves as that visibility boundary.

In production setups, PostGraphile is almost always configured with two database roles: an owner role for migrations, and a restricted role the GraphQL server actually connects with. This separation is not a nice-to-have, it is the foundation for the row-level security strategy described further below. Without a dedicated, restricted database role, PostGraphile cannot enforce meaningful access control through policies, because the connection would otherwise run with full privileges.


# Install PostGraphile CLI and PostgreSQL driver
npm install --save postgraphile pg

# Start a PostGraphile server against a local database,
# watch mode reloads the schema on every DB change
npx postgraphile \
  --connection postgres://app_user:secret@localhost:5432/shop \
  --schema public \
  --watch \
  --enhance-graphiql \
  --port 5678

# Production start (no watch mode, explicit role separation)
npx postgraphile \
  --connection postgres://owner:secret@db-host:5432/shop \
  --schema public \
  --default-role app_anonymous \
  --jwt-secret "$JWT_SECRET" \
  --jwt-token-identifier public.jwt_token

The --default-role parameter sets which PostgreSQL role unauthenticated requests run as. This role usually gets read-only access to public tables, while authenticated requests switch to a different role via a JWT. This pattern replaces entire auth middleware stacks that would otherwise need to sit in front of every resolver by hand, PostGraphile handles the role switch directly at the database level.

3. How PostGraphile introspects the schema

PostGraphile's introspection runs at server start and reads the PostgreSQL catalog: tables, columns, data types, primary keys, foreign keys, unique constraints and check constraints. Each table becomes a GraphQL type, each column becomes a field with the matching GraphQL scalar, PostgreSQL text becomes String, integer becomes Int, jsonb becomes its own JSON scalar. Foreign keys automatically become bidirectional relationship fields, an orders table with a foreign key to customers produces both a customer field on Order and an ordersByCustomerId field on Customer.

Especially valuable is the use of PostgreSQL comments as a documentation source. A COMMENT ON COLUMN in the database automatically shows up as a description in the GraphQL schema, visible in GraphiQL and in every introspection tool. This means documentation is maintained directly next to the data structure, rather than in a separate schema definition file that easily goes stale. This closeness between structure and documentation is one reason why PostGraphile APIs are often better documented in practice than hand-written alternatives.


-- This SQL schema becomes a fully typed GraphQL API automatically
create table app_public.customers (
  id serial primary key,
  email text not null unique,
  full_name text not null,
  created_at timestamptz not null default now()
);
comment on table app_public.customers is 'Registered shop customers.';
comment on column app_public.customers.email is 'Unique login email address.';

create table app_public.orders (
  id serial primary key,
  customer_id integer not null references app_public.customers(id),
  total_cents integer not null check (total_cents >= 0),
  status text not null default 'pending'
    check (status in ('pending', 'paid', 'shipped', 'cancelled')),
  created_at timestamptz not null default now()
);
comment on table app_public.orders is 'Customer orders with lifecycle status.';

-- After (re)start, PostGraphile exposes:
-- type Customer { id, email, fullName, createdAt, ordersByCustomerId(...) }
-- type Order { id, customerId, totalCents, status, customer, createdAt }

4. Row-level security as access control instead of resolver code

Without hand-written resolvers, PostGraphile needs a different place for access control, and that place is PostgreSQL itself. Row-level security, RLS for short, lets you define policies directly at the table level that determine which rows a given database role may see or modify. Instead of checking in a resolver whether the current user has access to an order, you define a policy in PostgreSQL that enforces exactly that, regardless of how the table is accessed.

This approach has a decisive security advantage over resolver-based authorization: the policy applies to every query, every mutation and every direct SQL access equally, there is no code path that can forget to run the check. In PostGraphile, RLS is usually combined with a session variable set from the JWT at the start of a request, typically the current user ID. The policy then compares table rows against that session variable.


-- Enable row-level security and restrict orders to their own customer
alter table app_public.orders enable row level security;

create policy select_own_orders on app_public.orders
  for select
  using (customer_id = current_setting('jwt.claims.customer_id')::integer);

create policy insert_own_orders on app_public.orders
  for insert
  with check (customer_id = current_setting('jwt.claims.customer_id')::integer);

-- Grant table access to the authenticated role, RLS still restricts rows
grant select, insert on app_public.orders to app_customer;
grant usage, select on sequence app_public.orders_id_seq to app_customer;

It is important that grants and policies work together: GRANT decides whether a role can access a table at all, RLS policies decide which rows are visible from that. A common mistake is enabling RLS but forgetting matching grants, which in practice results in empty result sets rather than error messages, which can be confusing during debugging.

5. Custom queries and mutations with PostgreSQL functions

Not every operation maps to simple CRUD on a table. For more complex logic, such as creating an order with multiple line items in a transaction, PostGraphile uses PostgreSQL functions. Every function in the exposed schema automatically becomes a GraphQL query or mutation, depending on whether it is marked VOLATILE, STABLE or IMMUTABLE. Volatile functions become mutations, stable and immutable functions become queries.

This mechanism allows complex business logic to stay close to the data without falling back to hand-written GraphQL resolvers. A PL/pgSQL function can modify multiple tables within a single transaction, offering consistency guarantees that are harder to achieve at the application level. For PostGraphile, such a function is simply another endpoint in the generated schema, complete with parameter and return-value type checking.


-- A volatile function becomes a GraphQL mutation automatically
create function app_public.place_order(
  customer_id integer,
  line_items jsonb
) returns app_public.orders as $$
declare
  new_order app_public.orders;
  item jsonb;
begin
  insert into app_public.orders (customer_id, total_cents, status)
  values (customer_id, 0, 'pending')
  returning * into new_order;

  for item in select * from jsonb_array_elements(line_items) loop
    insert into app_public.order_items (order_id, sku, quantity, price_cents)
    values (
      new_order.id,
      item->>'sku',
      (item->>'quantity')::integer,
      (item->>'priceCents')::integer
    );
  end loop;

  update app_public.orders
  set total_cents = (
    select coalesce(sum(quantity * price_cents), 0)
    from app_public.order_items where order_id = new_order.id
  )
  where id = new_order.id
  returning * into new_order;

  return new_order;
end;
$$ language plpgsql volatile security invoker;

comment on function app_public.place_order is 'Places a new order with multiple line items in one transaction.';

6. Plugins and schema extensions

Not every requirement can be solved purely in SQL, so PostGraphile offers a plugin system that lets you extend the generated schema programmatically. Plugins can add new fields, rename existing ones, hide fields, or introduce entirely new types that do not come directly from a table, such as a computed summary that calls external services. The plugin system is built on graphile-build and hooks into various phases of schema construction.

In practice, plugins are mostly used for three cases: renamings that deviate from PostgreSQL naming conventions, integrating external data sources that do not live in PostgreSQL, and additional validation that goes beyond database constraints. A common pattern is a field computed from several columns without that computation needing its own PostgreSQL function, for example a formatted display version of a price for the frontend.


// A small PostGraphile plugin adding a computed, non-SQL field
const { makeExtendSchemaPlugin, gql } = require('graphile-utils');

module.exports = makeExtendSchemaPlugin(() => ({
  typeDefs: gql`
    extend type Order {
      formattedTotal: String! @requires(columns: ["totalCents"])
    }
  `,
  resolvers: {
    Order: {
      formattedTotal: (order) => {
        // Simple currency formatting, no extra database round trip
        return (order.totalCents / 100).toFixed(2) + ' EUR';
      },
    },
  },
}));

7. Performance: how PostGraphile avoids N+1 problems

A reasonable concern with automatically generated APIs is performance, especially the classic N+1 problem with nested relationships. PostGraphile addresses this not with an application-level DataLoader pattern, but by translating a single GraphQL query into a single, often deeply nested SQL query using lateral joins. Instead of sending a separate query to the customers table for every order, PostGraphile generates one SQL query that pulls all needed data from PostgreSQL in one pass.

This approach works well as long as the underlying tables are properly indexed. Missing indexes on foreign key columns are the most common cause of performance issues in PostGraphile setups, because the generated lateral joins then fall back to sequential scans. For fields coming from custom functions, this optimization only applies partially, it's worth checking EXPLAIN ANALYZE on the generated query, which PostGraphile outputs in debug mode.


# A nested query like this compiles to ONE SQL statement with lateral joins,
# not one query per customer plus one query per order (no N+1)
query RecentOrdersWithCustomers {
  allOrders(first: 20, orderBy: CREATED_AT_DESC) {
    nodes {
      id
      totalCents
      status
      customer {
        id
        fullName
        email
      }
    }
  }
}

8. PostGraphile in production: deployment and versioning

For production use, PostGraphile should not run in watch mode, constantly reloading the schema wastes resources unnecessarily and isn't designed for live traffic. Instead, the schema is regenerated once per deployment, often as a Docker image with a fixed PostgreSQL connection built in. Migrations run through a separate tool, typically graphile-migrate, which was built specifically for the PostGraphile workflow and versions migrations as plain SQL files.

An often underestimated point is schema versioning: because the GraphQL schema follows directly from the database structure, every database migration is a potential breaking change in the GraphQL schema. A team running PostGraphile in production therefore needs the same discipline around migrations as with classic API versioning, additive changes first, removals only after a deprecation period. Monitoring tools like pg_stat_statements help catch slow generated queries early, before they cause problems in production.

9. PostGraphile compared to a hand-written API

The choice between PostGraphile and a classic, hand-written GraphQL API is not purely a matter of taste, it depends heavily on where business logic should live and how stable the data model is.

Criterion PostGraphile Hand-written API
Time to API Minutes to hours Days to weeks
Access control Row-level security in PostgreSQL Resolver guards, arbitrarily flexible
Complex domain logic PL/pgSQL functions, plugins Any application language
Integrating external services Only via plugins Native in the resolver
Schema stability Follows the DB structure directly Decoupled from the data model

In practice, PostGraphile is particularly well suited for internal tools, admin backends and prototypes where development speed matters more than maximum flexibility in API design. For public APIs with many external consumers, where the internal data model needs to change faster than the public interface, an additional decoupling layer usually makes more sense, either as a thin resolver layer in front of PostGraphile or as a fully hand-written API.

Mironsoft

GraphQL architecture, schema design and API performance

Want a GraphQL API derived directly from your database?

We review your PostgreSQL schema, design row-level security policies, and set up a production-ready PostGraphile installation with custom mutations and monitoring.

Schema review

Checking data model, indexes and constraints for PostGraphile readiness

RLS design

Row-level security policies for clean, auditable access control

Production setup

Deployment, monitoring and migrations with graphile-migrate

10. Summary

PostGraphile generates a complete, type-safe GraphQL API directly from a PostgreSQL schema without writing resolvers by hand. Tables become types, foreign keys become relationships, constraints become validation. Access control runs through row-level security directly in PostgreSQL instead of resolver guards, making security rules independent of the access path. Complex business logic lives in PL/pgSQL functions that automatically become mutations or queries, and can be extended further via plugins when needed.

Performance-wise, PostGraphile translates nested GraphQL queries into efficient SQL queries with lateral joins instead of many individual queries, as long as the database is properly indexed. For internal tools, admin interfaces and quick prototypes, the approach is often the fastest route to a production GraphQL API, for public APIs with many external consumers, an additional decoupling layer between data model and interface pays off.

PostGraphile — The essentials at a glance

Schema generation

Tables, columns, foreign keys and comments automatically become GraphQL types, fields, relationships and descriptions.

Access control

Row-level security in PostgreSQL replaces resolver guards and applies identically to every access path.

Custom logic

PL/pgSQL functions automatically become mutations or queries, depending on their volatility marker.

Performance

Lateral joins instead of N+1 queries, proper indexing on foreign keys is a prerequisite.

11. FAQ: PostGraphile

1What is PostGraphile?
A tool that automatically generates a complete GraphQL API from a PostgreSQL schema, including types, relationships and access control via RLS.
2Do I need my own schema file?
No, the schema is derived from the PostgreSQL catalog at runtime. Extra code is only needed for plugin extensions.
3Access control without resolvers?
Through row-level security in PostgreSQL, applying equally to GraphQL and direct SQL access.
4Mutations with complex logic?
Through PL/pgSQL functions marked VOLATILE, which PostGraphile automatically recognizes as mutations.
5Does it solve N+1?
Yes, via lateral joins instead of individual queries, provided foreign key columns are indexed.
6Combining with external services?
Yes, via plugins that add extra resolver fields calling external APIs.
7Suitable for public APIs?
To some extent, every DB migration is a potential breaking change. A decoupling layer helps with many external consumers.
8Migration management?
Usually with graphile-migrate, versioned plain SQL migration files matching the PostGraphile workflow.
9Watch mode in production?
No, watch mode is for development only. In production the schema is generated once at deployment.
10Required PostgreSQL version?
Usable from PostgreSQL 10, PostgreSQL 14 or newer is recommended for stable RLS performance.