Running EndpointTrust
Two services, one database, one secret.
The two services
| What it is | Who talks to it | Why separate | |
|---|---|---|---|
apps/device-api | The device-facing API: five contract operations plus two pairing operations | Windows endpoints | It is on the Windows sign-in path. If it is unavailable, sign-ins degrade to whatever a tenant's grace policy allows. |
apps/dashboard | The administrator console and the Stripe webhook | Administrators' browsers, Stripe | A 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
- Node 22 or later.
- PostgreSQL 18 (16 and 17 work; the schema uses nothing exotic).
- An SMS provider: either credentials for the managed sender, or each tenant brings their
own.
- Stripe, if you intend to charge.
The master key
MASTER_KEY is 32 bytes, base64. It encrypts:
- every tenant's ES256 private signing key, and
- every tenant's SMS provider credentials.
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
- [ ]
MASTER_KEYbacked up separately from the database. - [ ]
API_ORIGINis the final HTTPS origin. Endpoints refuse anything else. - [ ] TLS terminated in front of both services. The endpoint refuses plain HTTP and refuses
to bypass certificate validation.
- [ ]
STRIPE_WEBHOOK_SECRETset, and the webhook endpoint registered in Stripe. - [ ] Placeholder plan ids and grace policy replaced. See
- [ ] An agent release published, with its real SHA-256.
- [ ] Database backups tested by restoring one.
- [ ]
tenants.enrollment_modeleft atadmin_onlyunless you have read
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.