Backend
Self-host the backend
Before you start
Inth runs this backend for you. Self-host when you need to operate the service and its database yourself. You then own provisioning, migrations, backups, policy configuration, signing secrets, availability and upgrades.
Your c15t app talks to a self-hosted backend exactly as it talks to Inth, through hosted mode. Only the backend URL changes.
Install the backend
Also install the driver for your database. The backend declares each driver as an optional peer dependency pinned to one version; install that version.
| Database | Driver |
|---|---|
| PostgreSQL | @effect/sql-pg |
| MySQL | @effect/sql-mysql2 |
| SQLite | @effect/sql-sqlite-node |
Configure it
Create c15t-backend.config.ts in the app that will serve the backend:
This is a local development configuration. trustedOrigins lists the sites
whose browsers may call the backend; add your production host before you
deploy. Keep the SQLite file on persistent storage, because a temporary
filesystem loses consent history. Database setup
covers PostgreSQL and MySQL, and policy configuration
covers rules for other regions.
Create the database schema
Run the migration with the same configuration the server uses:
Review the plan before you apply it, and back up an existing database first. Self-hosted migrations explains the report.
Mount the request handler
c15tInstance(options).handler takes a web Request and returns a Response.
Mount it on a catch-all route and route GET, POST, PUT, PATCH and
OPTIONS to it. PUT publishes legal-document releases; a route that exports
only POST leaves the manifest, /init and subject reads unreachable.
Create one instance per server process and reuse it, so requests share its
database connection pool. Set basePath to the path prefix of the requests the
handler receives, so /api/c15t/init routes to /init.
Next.js App Router
This route needs a Node.js server. A Next.js static export cannot run it; deploy the backend separately and give the app its absolute URL.
examples/self-host
runs this route with an in-process PGlite database in development and
PostgreSQL from DATABASE_URL when deployed. Its banner calls the backend at
the same-origin path /api/c15t.
TanStack Start
The TanStack Start example mounts the backend at /api/self-host as a server
route. Its getInstance() creates one c15tInstance() on first use and reuses
it:
Nuxt
The Nuxt example mounts the backend at /api/self-host as a Nitro catch-all
route, converting the h3 event with toWebRequest():
SvelteKit
The SvelteKit example creates its c15tInstance() as backend in
src/lib/server/c15t-backend.ts. The route at /api/self-host imports that
module dynamically on the first request and forwards each request to it:
Keep the import dynamic on SvelteKit 3. A static import lets the build place
modules the backend shares with its lazily loaded chunks in the route's own
chunk, and SvelteKit then fails the build with Invalid export for the route.
Another server
Pass the incoming web Request to handler and return its Response
unchanged, keeping status, body and headers such as CORS and cache headers. If
your framework has its own request type, convert it with the framework's web
request adapter. If a parent router already strips the prefix, leave basePath
unset rather than stripping it twice.
Tests and one-off scripts should call instance.dispose() when they finish to
close the connection pool. A long-running server does not need to.
Point your app at the backend
Follow your framework quickstart and use your backend's URL,
such as https://app.example.com/api/c15t, where it asks for the Inth URL.
Server-side resolution needs an absolute URL. The browser only needs the HTTP
endpoint; keep the config file and database credentials on the server.
The frontend does not have to run the backend. A static site or an edge-rendered
app can call a backend deployed elsewhere, as long as its origin is in
trustedOrigins.
Check the deployment
- Request
/statusand/manifestthrough the public mount point, for examplecurl -i https://app.example.com/api/c15t/status./statusreturns200once the database answers and the schema exists. The manifest lists your policy rules. - Load the app from a trusted origin in a private window. The banner appears for a visitor your policy asks to choose.
- Accept, then check DevTools Network for a
POSTto/subjectsthat returns a success status. - Reload. The banner stays closed and your choice is still applied.
- For a cross-origin deployment, check that
/initgoes out without anOPTIONSpreflight and the consent save'sOPTIONSpreflight succeeds. An/initpreflight means something added a custom request header, such as the hosted transport'sheadersoption.
Neither /status nor /manifest writes to the database, so only steps 3 and 4
prove that consent writes and reads work. Continue with configuration
and HTTP endpoints.