Running EndpointTrust

Two services, one database, one secret.

The two services

What it isWho talks to itWhy separate
apps/device-apiThe device-facing API: five contract operations plus two pairing operationsWindows endpointsIt is on the Windows sign-in path. If it is unavailable, sign-ins degrade to whatever a tenant's grace policy allows.
apps/dashboardThe administrator console and the Stripe webhookAdministrators' browsers, StripeA deploy here, or a crash in a chart renderer, must not be able to interrupt the service above.

They share packages/db and packages/core and nothing else. Deploy them separately, scale

them separately, and give the device API the smaller blast radius.

Prerequisites

own.

The master key

MASTER_KEY is 32 bytes, base64. It encrypts:

It is never in the database, so **a database dump alone cannot sign a licence or read a

tenant's SMS credentials**. That is the whole point.


node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Losing it means every tenant needs a new signing key and therefore a new configuration

bundle on every machine. Back it up somewhere a database restore cannot reach, and keep it

out of the same backup as the database.

First run


cp .env.example .env     # then fill in MASTER_KEY, API_ORIGIN, DASHBOARD_ORIGIN
npm install
npm run build
npm run db:dev           # PostgreSQL 18, no Docker and no root. Leave it running.
npm run db:migrate       # the committed migrations
npm run db:seed          # the plans. Read docs/commercial-placeholders.md first.
npm run db:seed:demo     # a populated demo organisation, for development only
npm run dev:api          # :8080
npm run dev:dashboard    # :3000

npm run db:up is the Docker Compose alternative to db:dev, for people who have a

working docker socket. On a stock Arch install that needs the daemon started

(systemctl start docker) and the user added to the docker group, both of which need

root — which is exactly why db:dev exists and why the test suite uses the same

embedded binaries.

Every entry point loads .env through Node's own --env-file; there is no dotenv

dependency and nothing reads a config file at runtime.

API_ORIGIN is written into every signed licence, and endpoints talk to exactly that

origin and refuse a licence naming another. Changing it after licences are issued means

reissuing every one of them and redistributing every bundle. Decide it before you sell

anything.

Stripe


stripe listen --forward-to localhost:3000/webhooks/stripe

Set STRIPE_WEBHOOK_SECRET to what that prints. Then replace the placeholder product and

price ids in packages/db/src/plans.ts with your own and re-run npm run db:seed.

Without STRIPE_SECRET_KEY the whole console works except checkout, seat changes and

cancellation, and the pages say so rather than offering buttons that fail. Without

STRIPE_WEBHOOK_SECRET the webhook route answers 503 — which means a completed checkout

will never issue a licence, so do not run that way in production.

The one rule about entitlement

Nothing a browser can send changes what an organisation is entitled to. Seats come from

subscriptions.seats, and only a signature-verified webhook writes that column. A form post

claiming ten thousand seats is a form post that changes nothing.

Webhook events are applied at most once: Stripe's own event id is the primary key of

stripe_events, so a redelivery is a no-op. A failure answers 500 so Stripe retries;

answering 200 to an event that could not be applied would silently drop a subscription

change.

Migrations

Generated with drizzle-kit and committed. npm run db:migrate applies them, recording each

filename in _endpointtrust_migrations, one transaction per file — a half-applied migration is

the one outcome from which there is no automatic recovery. The test harness applies the

same committed files rather than pushing the schema, so a migration that does not apply

fails the test suite instead of surfacing at deploy time, and a CI job fails if the schema

changes without one.


npm run db:generate   # after editing packages/db/src/schema.ts
npm run db:migrate    # apply what is new

Production checklist

to bypass certificate validation.

commercial-placeholders.md.

pairing-security.md.

What this service does not do

No scheduler, no queue, no background workers. Session pruning happens opportunistically on

login. There is nothing to run besides the two processes, which is deliberate: an

administrative console that needs a job runner to stay correct is a console that breaks

quietly when the job runner stops.