Deployment
Build, migrate, release, scale, and operate KetJS HTTP and worker processes safely.
A KetJS release is an emitted JavaScript workspace plus the packages, migrations, configuration, and external services selected by that workspace. The HTTP server and durable worker are separate process roles over the same composed manifest and datastore.
Release pipeline#
Diagram source
%% File: packages/docs/content/docs/deployment.md
flowchart LR
source["TypeScript source"] --> build["Emit JavaScript"]
build --> check["ket check"]
check --> tests["Headless tests"]
tests --> plan["Migration dry run"]
plan --> migrate["Apply migrations"]
migrate --> http["Start HTTP role"]
migrate --> worker["Start worker role"]
http --> verify["Health and smoke checks"]
worker --> verifyDo not execute TypeScript loaders in production. Compile the workspace and every imported module, theme, route, job, and tenant provider to paths that remain valid in the deployment artifact.
Build and validate#
An application normally provides project-specific scripts around this sequence:
# Run from: /path/to/example-app
npm ci
npm run build
ket check --workspace dist/ket.workspace.js
ket test dist/test
ket migrate --deployment erp --workspace dist/ket.workspace.jsUse ket snapshot and ket diff when a release process reviews manifest changes. Treat a clean manifest
diff as necessary but not sufficient: schema compatibility is determined against the actual datastore.
Apply migrations before traffic#
For one datastore, ket migrate is a source-controlled planning aid: it compares the manifest with
.ket/schema.<app>.json, prints SQL, and advances that snapshot. Apply the reviewed plan with an
application-owned deployment script that opens the real adapter and calls migrateOne(), or let one
controlled boot migrate before normal replicas start.
For a tenant fleet, the CLI opens each database and owns the apply step:
# Run from: /path/to/ketjs
ket migrate --deployment erp --workspace dist/ket.workspace.js --all --dry-run
ket migrate --deployment erp --workspace dist/ket.workspace.js --allSet KET_MIGRATE=0 on normal application pods after the release system owns migration order. This prevents
many replicas from racing to alter the same schema during rollout. Destructive plans require an explicit
--allow-destructive decision and should include a restore plan.
Run separate roles#
# Run from: /path/to/example-app
ket serve --deployment erp --workspace dist/ket.workspace.js
ket worker --deployment erp --workspace dist/ket.workspace.jsScale HTTP replicas for request load. Scale worker replicas for queue throughput; each replica still follows
the per-queue concurrency declared by worker.queues. Database leases make claims durable across crashes.
Keep HTTP and worker deployments on the same release artifact during a schema or job-contract transition. If a rolling deployment allows old and new code to overlap, make database and payload changes backward compatible for that interval.
Required production decisions#
Before starting traffic, decide and configure:
- a datastore adapter and connection policy;
- a stable
KET_SECRETshared by every HTTP replica; - a separate
KET_WEBHOOK_SECRETwhen anonymous callbacks are enabled; - local or S3-compatible object storage;
- worker queues, concurrency, timeouts, retries, and shutdown grace;
- tenant catalogue and bounded adapter pool for tenant deployments;
- the exact module composition for each deployment;
- logs, metrics, readiness, backups, and disaster recovery.
Do not place durable local SQLite or object-storage files in an ephemeral container layer. Mount persistent storage or choose external services.
PostgreSQL#
Install the optional driver in the deployment package and make adapter ownership explicit:
// File: src/app.ts
import { defineDeployment } from '@ketvietlab/ketjs'
import { postgresAdapter } from '@ketvietlab/ketjs-postgres'
export const erp = defineDeployment({
name: 'erp',
modules: [core],
headless: true,
serve: {
openStore: async (config) => {
if (!config.databaseUrl) throw new Error('DATABASE_URL is required')
const adapter = postgresAdapter(config.databaseUrl, { max: 20 })
await adapter.open()
return adapter
},
},
})Install both @ketvietlab/ketjs-postgres and its optional postgres peer dependency. The application owns the driver
dependency, opens the adapter in openStore, and returns it ready for use; KetJS core owns only the
Adapter contract. Graceful shutdown closes the returned adapter.
S3-compatible storage#
Configure KET_STORAGE=s3 plus endpoint, region, bucket, and credentials. KetJS signs requests directly;
provider networking and credential distribution remain deployment responsibilities. Prefer workload
identity or a secret manager over credentials in an image or repository.
Object storage is namespaced per app or tenant at runtime. Preserve the namespace contract when moving data between environments.
KET_STORAGE defaults to local, and KetJS does not refuse it in production. On pods without a
persistent volume, local storage loses every upload when the pod restarts. Set KET_STORAGE=s3 for
containerised deployments, or mount a durable volume at KET_STORAGE_DIR for a single host.
With object storage, an upload is durable once POST /files answers 201. The request spools the
body to a temporary directory only to compute its checksum, writes the object to storage, and then
commits the attachment row together with its follow-up jobs (publication copy, image renditions).
The temporary directory is removed before the response. Those jobs read the original back from
storage, so any worker pod can run them. If a pod stops mid-request, the client sees a failed upload.
An object written before the row was committed has no attachment row, and the storage sweep removes
it once it is old enough.
Health and graceful shutdown#
Expose a small application route for readiness, for example:
// File: src/app.ts
serve: {
routes: () => ({
'/health': () => json({ ok: true }),
}),
}Readiness should prove that the process has completed composition and boot. Add a datastore check only if the orchestrator's request rate will not overload a dependency during an incident.
On shutdown:
- stop accepting new HTTP work or job claims;
- allow in-flight requests and jobs to finish within their grace periods;
- stop heartbeats and polling;
- close sessions, storage/provider resources, adapters, and sockets;
- let expired job leases become retryable if a worker cannot finish.
Configure the platform termination grace period longer than the deployment worker's shutdownGraceMs.
Rollback strategy#
Code rollback is safe only while the previous release understands the current schema and durable payloads. Prefer additive changes in one release, migrate producers and consumers in another, then remove obsolete fields later. For a destructive migration, restore data or deploy a forward fix; reverting code alone does not recreate dropped data.
Production checklist#
- The artifact contains emitted ESM and no production TypeScript loader.
ket checkand targeted headless tests pass against the artifact.- Migration output was reviewed against representative data.
- HTTP and worker roles use the same app, manifest, secrets, and datastore.
- Sessions survive pod changes and restarts.
- Tenant pools are bounded and unknown tenants fail closed.
- Queue concurrency matches provider and database limits.
- Persistent storage, backups, log retention, and secret rotation are defined.
- Readiness and shutdown behavior were exercised during a rolling deployment.