Skip to main content
This page builds the smallest Keja service. Each step does one thing and links to the page that explains it. Install covers the GitHub Packages token and bun add.

Create the project

keja init copies the starter into an empty directory, writes .env from .env.example, runs bun install, runs git init, and adds src/public/hello.ts plus src/service/ping.ts.
The starter turns Postgres on. The first request on this page does not need a database, so open keja.config.ts and set db to { enabled: false } until the data section below. Project lists the other config fields. .env already sets HOST, HTTP_PORT, and HTTP_SERVICE_PORT. The starter values are 127.0.0.1, 3000, and 3001.
keja dev prints no issues found when the project loads. Request the public port:
The body is { "message": "hello" }. src/public/hello.ts is the route that produced it:
path is /hello. The /v1 in the URL is the version, from the next section. Saving this file reloads the route. keja dev prints reloading... and then no issues found.

The version in the URL

keja.versioning.ts is the catalog. The starter publishes one version:
The first segment of a request path is that version. GET /v1/hello asks for v1 and the route path /hello. Versioning explains what happens when a later version exists and this path does not.

Change the greeting

Add a query parameter so the handler reads the request. Routes describes request. Responses describes respond.Hello.
The body is { "message": "hello Ada" }. A request with no name returns HTTP 400.

The second server

The same process listens on HTTP_SERVICE_PORT. Service routes are POST. The starter’s src/service/ping.ts is POST /ping.
The body is { "ok": true }. Servers lists the reserved routes on both ports, including /_health and /docs.

When the service stores data

Turn Postgres back on when a route needs to store a row. OwnedObjects is the column shape. TypeIDs is the _id. Database is migrations, repositories, and seed data. Set db to { enabled: true } in keja.config.ts. .env needs a postgres:// or postgresql:// DATABASE_URL. Restart keja dev after that change so the pool opens. The database pool stays as it was at boot. The starter already has migrations/0001_init.sql. Add the notes table as the next file, migrations/0002_notes.sql:
keja.repositories.ts turns on list for that table:
Apply the SQL, then generate the repository. Generate reads the migration files. It does not connect to Postgres.
keja generate writes src/db/repositories.ts. A route attaches notes.list.schema and passes the tenant itself. The tenant is a value your service already knows. This handler uses a fixed one so the first list runs:
Save that file as src/public/notes.ts. After the reload:
The body is { "notes": [], "nextCursor": null } until a row exists. notes.meta is the stored row, including _id and the timestamps. notes.schema is the domain columns only, here message. keja seed inserts starter rows. The Database page shows defineSeeder. Argus, Vault, and Cloudflare Access stay off until a service needs them.