To self-host Constructive, create two dictionary secrets (Control Plane secrets that hold named key-value pairs), one for the PostgreSQL superuser and one for a separate server login, and install the Constructive template from the Template Catalog, Control Plane’s library of ready-to-install apps. You get a GraphQL API generated from your Postgres schema, reachable only from inside Control Plane, where every request runs as a non-superuser role so row-level security decides which rows each user sees.
Most APIs guard data with application code that checks, endpoint by endpoint, whether this user may read that row. Miss one check and the data leaks. Constructive (formerly LaunchQL) moves the rule into the database. It pairs pgpm, a package manager that versions schemas, row-level security (RLS) policies and functions as Postgres modules, with a GraphQL server built on PostGraphile that generates the API from the database. Both are MIT-licensed. Running it safely takes two database logins, an identity header that must never face the internet, and modules that deploy in order. The template handles all three.
Why self-host Constructive on Control Plane
-
No public endpoint, by design. The server trusts the user named in the
X-Actor-Idheader, so the template never publishes the server to the internet. -
You pick the callers.
internalAccessadmits workloads in the same Global Virtual Cloud (GVC), Control Plane’s term for the set of locations a workload runs in, the whole org, or only named workloads, such as an internal service or an AI agent querying for a user. - Two secrets, read by name. The database superuser and the server login live in Control Plane secrets and stay out of Helm values and your install command.
- An identity scoped to those two. A Control Plane policy created by the template lets the server’s Control Plane identity reveal your two secrets and nothing else.
-
A login that cannot skip the rules. The server connects as
NOSUPERUSER NOBYPASSRLS, and the superuser credentials leave its environment before it starts. - Database included. PostgreSQL 18 with PostGIS and pgvector comes along, with optional scheduled backups to your own S3, GCS or MinIO bucket.
Included out of the box
- A GraphQL server on port 3000 at
/graphql: one replica by default, up to four - GraphiQL at
/graphiqlfor browsing the generated types and mutations - PostgreSQL 18, PostGIS and pgvector on a 10 GiB volume set, Control Plane’s managed persistent storage for a workload
- The
anonymousandauthenticatedrequest roles and the server login, applied on every boot - A minimal set of Constructive’s MIT-licensed platform modules, then yours, each deployed only when its configuration changed
- Internal DNS at
RELEASE_NAME-constructive.GVC_NAME.cpln.local - Built-in Control Plane logs and metrics, with tracing available to turn on
How to deploy a Postgres schema as code with pgpm
Set modules.source to git, point modules.git.url at the https URL of a public repository holding a pgpm workspace, set modules.git.ref to a tag or full commit SHA, and list your packages in modules.packages in deploy order. pgpm deploy fetches no dependencies, so commit everything your packages require (the pgpm install output).
To ship a change, push a tag, update the ref and run cpln helm upgrade (cpln is Control Plane’s CLI). A restart with unchanged settings skips the deploy, so a branch name never picks up commits silently. If a deploy fails, the new replica stops with the pgpm error in its logs while the old replicas keep serving. To keep sources private, bake the workspace into an image built FROM constructiveio/constructive:08e422d, then set image to your image, modules.source: image and modules.path to the workspace’s absolute path inside it.
How row-level security decides what each user sees
Each call sends X-Schemata (the schemas to expose), X-Database-Id (a fixed value, the same on every install) and, optionally, X-Actor-Id, the acting user’s UUID. With an actor, the request runs as authenticated with jwt.claims.user_id set to that UUID, so your policies see that user. Without one, it runs as anonymous. Your modules grant those two roles what they may touch, and a table with no grant answers permission denied. Per-user isolation becomes SQL you can review in a pull request.
The server believes X-Actor-Id as sent, and X-Schemata can name any schema the server login can read. Authenticate the user in the calling service before it sets the header, and have that service set X-Schemata to a fixed list. The default, same-gvc, lets every workload in the GVC call as any user, so set internalAccess.type to workload-list and name your trusted callers when the GVC runs anything else. Traffic between workloads uses mTLS.
How to browse the API without a public endpoint
cpln port-forward RELEASE_NAME-constructive 3000:3000 --gvc GVC_NAME brings the server to localhost:3000 and exposes nothing. GraphiQL needs the X-Database-Id and X-Schemata headers on the page request itself, which a browser does not send, so add them with a header-setting extension or a small local proxy. To query as a user, set X-Actor-Id in the GraphiQL Headers pane.
How to back up Constructive’s Postgres database
Backups are off by default. Set postgres.backup.enabled: true, choose aws, gcp or minio as postgres.backup.provider, and fill in that provider’s bucket. A cron workload then runs pg_dumpall daily at 02:00 UTC by default and writes a compressed SQL dump to your bucket, off the volume. AWS and GCP need a Control Plane cloud account (plus a bucket-scoped IAM policy on AWS); MinIO needs a dictionary secret with accessKey and secretKey.
To restore, load the dump as the superuser into an empty database, as the postgres template docs describe. Keep postgres.image on an image with PostGIS: Constructive’s modules require it, and a backup restores only into such a server. Rehearse a restore before you rely on it. Uninstalling deletes the database volume, so confirm a recent dump first.
How to scale Constructive and the limits to plan around
API replicas hold no state, so replicas: 2 keeps the API up through restarts and upgrades. The ceiling is four, because each replica holds its own connection pool and the bundled PostgreSQL allows 100 connections.
The database is one instance, so a database restart interrupts the API at any replica count. Install into a GVC with exactly one location, because the bundled database runs in one location. Jobs that modules enqueue are not processed (the template runs no job worker), and the first cpln helm upgrade after an install can restart the API.
How to deploy Constructive on Control Plane
Sign up for Control Plane and create the two dictionary secrets: username, password and database for PostgreSQL, and username and password for the server login, with a different username. Secret names are org-wide, so give each release its own. If either is missing, the deployment waits with empty logs, and cpln workload get-deployments names the missing one. Then choose Constructive in the Template Catalog in the Control Plane Console, or install with the cpln CLI:
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/constructive \
--version 1.0.0 --gvc GVC_NAME \
--set postgres.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME \
--set server.loginSecretName=LOGIN_SECRET_NAME
The Constructive template documentation covers module layout, password rotation, backups and troubleshooting.
FAQ
Answers to common questions about running Constructive on Control Plane.
How do I self-host Constructive?
Create two dictionary secrets, one for the PostgreSQL superuser and one for the server login, then install the Constructive template from the Template Catalog or with cpln helm install. The template creates the server, PostgreSQL and its volume set.
What is Constructive?
Constructive, formerly LaunchQL, is an open-source toolkit for building on PostgreSQL. It combines pgpm, a package manager for versioned Postgres modules, with a GraphQL server built on PostGraphile that generates an API from the database.
How do I generate a GraphQL API from a Postgres database?
Write your schema, RLS policies and functions as pgpm modules and run Constructive’s GraphQL server against the database. On Control Plane, point the template at your modules’ git tag and it serves the generated API.
Is Constructive free and open source?
Yes for everything this template runs. The Constructive repository, including pgpm and the GraphQL server, is MIT-licensed, and the Control Plane template deploys only MIT-licensed code. Some Constructive platform packages come from a separate, non-public repository, and the template leaves them out.
Can I expose the Constructive API to the internet?
Not with this template. Any caller could claim to be any user through X-Actor-Id, so it has no public endpoint. Put a service in front of it in the same GVC that authenticates each user and sets X-Actor-Id and X-Schemata itself, dropping any copies the client sent, and admit only that workload.
How do I back up Constructive’s database?
Set postgres.backup.enabled: true and configure an S3, GCS or MinIO bucket. A cron workload writes a compressed pg_dumpall to the bucket daily at 02:00 UTC by default, which you restore as the superuser into an empty database.
Does Constructive on Control Plane scale?
The API runs up to four stateless replicas, a fixed count you set with replicas. PostgreSQL is a single instance in one location, so grow it vertically: raise postgres.resources and run cpln volumeset expand.
Can I run Constructive on Kubernetes?
Yes, without operating a cluster. cpln helm install uses a Helm chart and Control Plane runs the workloads. To use a cluster you already run, add it as a Bring Your Own Kubernetes (BYOK) location and install into a GVC with that one location.
How is Constructive different from PostGraphile or Hasura?
Like them, it generates GraphQL from a Postgres schema, and its server is built on PostGraphile. Constructive adds pgpm, which packages the schema itself as versioned modules with dependencies and deploys them in order, plus preset anonymous and authenticated roles so every request runs under RLS. PostGraphile supports per-request roles when you configure them, and Hasura enforces access with permission rules kept in its own metadata.
Originally published on the Control Plane blog.
Top comments (0)