feat(container): M-Box 1-8 — stack-agnostic standalone Docker container
Shade now ships as a self-contained Docker image. Deploy one container
per project, any stack (Bun, Python, Go, Rust, Kotlin) can talk to it via
plain HTTP. Zero coupling to consumer codebases.
M-Box 1: Stale identity cleanup API
- touchIdentity + purgeStaleIdentities on PrekeyStore interface
- Implemented for Memory, SQLite, and Postgres backends
- SQLite adds last_activity_at column with migration ALTER for existing DBs
- Postgres adds the same via raw SQL with IF NOT EXISTS guards
- Routes call touchIdentity on register, bundle fetch, replenish
- 4 new tests for the cleanup API
M-Box 2: Stale cleanup background task
- StaleCleanupTask runs purge on startup + every 24h (configurable)
- Reads SHADE_STALE_DAYS (default 30) and SHADE_CLEANUP_INTERVAL_HOURS
- Wired into standalone.ts, stopped on graceful shutdown
- 5 new tests for the task
M-Box 3: Observer baked into the container
- standalone.ts conditionally mounts @shade/observer at /shade-observer
when SHADE_OBSERVER_TOKEN is set (and >= 16 chars)
- Shared PrekeyServerEvents emitter feeds both routes and observer
- @shade/observer added as optional dependency of @shade/server
M-Box 4: Dockerfile with dashboard build
- Multi-stage build: oven/bun:1 builder → oven/bun:1-alpine runtime
- COPY packages/ wholesale so workspace lockfile resolves cleanly
- RUN bun run build inside shade-dashboard → dist/ → observer/dist/
- Non-root shade user, /data volume, healthcheck, env defaults
- Final image: 260 MB
M-Box 5: OpenAPI spec for stack-agnostic clients
- packages/shade-server/openapi.yaml documents all 9 endpoints with
request/response schemas, security (Ed25519 signatures + bearer token)
- createOpenApiRoutes serves /openapi.yaml and /docs (Redoc viewer)
- Any language can generate a client with openapi-generator
M-Box 6: Docker CI pipeline
- .gitea/workflows/docker.yml builds + pushes on git tag v*
- scripts/build-docker.ts for local builds, supports --push with GITEA_TOKEN
- Root package.json: build:docker, publish:docker scripts
M-Box 7: Deployment documentation
- packages/shade-server/README rewritten: 5-line quickstart with the image
- docs/DEPLOYMENT.md: full reference, env vars, backup, Dokploy, PG setup
- examples/05-dokploy-deployment/docker-compose.yml updated to pull
published image (gt.zyon.no/stian/shade-prekey:latest)
- Root README deployment section rewritten
M-Box 8: End-to-end verification
- Image builds locally (bun run build:docker)
- /health, /openapi.yaml, /docs, /metrics, /shade-observer all respond
- 401 without observer token, 200 with
- Real SDK client round-trip: Alice → container → Bob → reply → Alice
- Persistence: identity + prekeys survive container restart (count 20→18
as expected from two bundle fetches)
285 tests passing, 0 failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:29:00 +02:00
# @shade/server — Shade Prekey Server (standalone container)
A self-contained Docker image that provides the prekey server, OpenAPI contract, observer dashboard, and stale cleanup — **everything a project needs to adopt Shade ** , with zero coupling to the consumer's stack.
## Deploy in 2 minutes
```bash
docker run -d \
--name my-project-shade \
-v my-project-shade:/data \
-p 3900:3900 \
-e SHADE_OBSERVER_TOKEN=change-me-to-at-least-16-chars \
gt.zyon.no/stian/shade-prekey:latest
```
Done. Your prekey server is live:
- `http://localhost:3900/health` — health check
- `http://localhost:3900/openapi.yaml` — API contract for any language
- `http://localhost:3900/docs` — interactive API reference (Redoc)
- `http://localhost:3900/shade-observer/dashboard/` — live debugger (token required)
- `http://localhost:3900/v1/keys/*` — prekey REST API
Your consumer projects (Nova, Orchestrator, Python apps, anything) then point at `http://localhost:3900` as their `prekeyServer` URL.
## One container per project
The recommended architecture is **one Shade container per project ** :
```
nova-shade (Docker container, SQLite volume) ← Nova backend + Android app
orchestrator-shade (Docker container, SQLite volume) ← Orchestrator hub + workstations
future-project (Docker container, SQLite volume) ← Any future app
```
Each project owns its own container, its own volume, its own observer token. Zero cross-project coupling. If one project's Shade is down, the others keep running.
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
## Keys vs. payloads — what this server is, and isn't
The prekey server is a **public-key directory ** . It exists so a brand-
new client can find the right Ed25519 + X25519 bundle to start an
X3DH handshake with a peer it has never talked to. After that, the
peers ratchet directly.
What lives on this server:
- Identity public keys
- Signed prekey + one-time prekey bundles
- Activity timestamps (used by stale cleanup)
- Operator metadata: `/health` , `/metrics` , `/openapi.yaml` ,
`/shade-observer/*`
What never lives on this server:
- **Message plaintext.** Ratchet envelopes flow peer-to-peer.
- **Transfer chunks.** `@shade/transfer` POSTs ciphertext directly to
the receiver's `/v1/transfer/:streamId/chunk` route — not here.
- **Identity private keys** or **session state ** . Both are device-
local.
- **Resume secrets** for in-flight transfers. Encrypted under a
device-key derived from the identity signing key, never uploaded.
This is the bright line that lets you deploy one shared prekey
container per project even when consumer apps don't trust each other:
the worst a compromised prekey server can do is hand out a fake
bundle (MITM at first contact). Out-of-band fingerprint comparison
detects this — see `THREAT-MODEL.md § 2` and the `getIdentityFingerprint()`
API.
For deployment-time gates (TLS, backup, observer-token rotation, log
level, secret rotation) see
[`docs/PRODUCTION-CHECKLIST.md` ](../../docs/PRODUCTION-CHECKLIST.md ).
For the wire contract — including the peer-served
`/v1/transfer/*` and `ShadeTransferAuthenticator` security scheme —
see [`openapi.yaml` ](./openapi.yaml ).
feat(container): M-Box 1-8 — stack-agnostic standalone Docker container
Shade now ships as a self-contained Docker image. Deploy one container
per project, any stack (Bun, Python, Go, Rust, Kotlin) can talk to it via
plain HTTP. Zero coupling to consumer codebases.
M-Box 1: Stale identity cleanup API
- touchIdentity + purgeStaleIdentities on PrekeyStore interface
- Implemented for Memory, SQLite, and Postgres backends
- SQLite adds last_activity_at column with migration ALTER for existing DBs
- Postgres adds the same via raw SQL with IF NOT EXISTS guards
- Routes call touchIdentity on register, bundle fetch, replenish
- 4 new tests for the cleanup API
M-Box 2: Stale cleanup background task
- StaleCleanupTask runs purge on startup + every 24h (configurable)
- Reads SHADE_STALE_DAYS (default 30) and SHADE_CLEANUP_INTERVAL_HOURS
- Wired into standalone.ts, stopped on graceful shutdown
- 5 new tests for the task
M-Box 3: Observer baked into the container
- standalone.ts conditionally mounts @shade/observer at /shade-observer
when SHADE_OBSERVER_TOKEN is set (and >= 16 chars)
- Shared PrekeyServerEvents emitter feeds both routes and observer
- @shade/observer added as optional dependency of @shade/server
M-Box 4: Dockerfile with dashboard build
- Multi-stage build: oven/bun:1 builder → oven/bun:1-alpine runtime
- COPY packages/ wholesale so workspace lockfile resolves cleanly
- RUN bun run build inside shade-dashboard → dist/ → observer/dist/
- Non-root shade user, /data volume, healthcheck, env defaults
- Final image: 260 MB
M-Box 5: OpenAPI spec for stack-agnostic clients
- packages/shade-server/openapi.yaml documents all 9 endpoints with
request/response schemas, security (Ed25519 signatures + bearer token)
- createOpenApiRoutes serves /openapi.yaml and /docs (Redoc viewer)
- Any language can generate a client with openapi-generator
M-Box 6: Docker CI pipeline
- .gitea/workflows/docker.yml builds + pushes on git tag v*
- scripts/build-docker.ts for local builds, supports --push with GITEA_TOKEN
- Root package.json: build:docker, publish:docker scripts
M-Box 7: Deployment documentation
- packages/shade-server/README rewritten: 5-line quickstart with the image
- docs/DEPLOYMENT.md: full reference, env vars, backup, Dokploy, PG setup
- examples/05-dokploy-deployment/docker-compose.yml updated to pull
published image (gt.zyon.no/stian/shade-prekey:latest)
- Root README deployment section rewritten
M-Box 8: End-to-end verification
- Image builds locally (bun run build:docker)
- /health, /openapi.yaml, /docs, /metrics, /shade-observer all respond
- 401 without observer token, 200 with
- Real SDK client round-trip: Alice → container → Bob → reply → Alice
- Persistence: identity + prekeys survive container restart (count 20→18
as expected from two bundle fetches)
285 tests passing, 0 failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:29:00 +02:00
## Environment variables
| Var | Default | Description |
|-----|---------|-------------|
| `PORT` | `3900` | HTTP port |
| `SHADE_PREKEY_DB_PATH` | `/data/shade-prekeys.db` | SQLite file path |
| `SHADE_PREKEY_PG_URL` | unset | Postgres connection string. If set, overrides SQLite. |
| `SHADE_OBSERVER_TOKEN` | unset | Bearer token for the dashboard. Min 16 chars. Unset = observer disabled. |
| `SHADE_STALE_DAYS` | `30` | Purge identities with no activity in N days |
| `SHADE_CLEANUP_INTERVAL_HOURS` | `24` | How often the cleanup task runs |
| `SHADE_LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` |
## Persistence
The `/data` volume holds the SQLite database. Back it up by copying the `.db` file (use SQLite's online backup API or just stop the container briefly).
To switch to Postgres, set `SHADE_PREKEY_PG_URL=postgres://user:pass@host/db` . Tables will be created automatically with the `shade_server_*` prefix.
## Stale cleanup
Identities that have no activity (no bundle fetches, no replenishments, no registration updates) for more than `SHADE_STALE_DAYS` days are automatically purged. This keeps the database bounded even if users never unregister cleanly.
## Using from your project
Any language can speak to a Shade container — it's just HTTP. See [openapi.yaml ](./openapi.yaml ) for the full contract.
**TypeScript / Bun:**
```ts
import { createShade } from '@shade/sdk ';
const shade = await createShade({ prekeyServer: 'http://my-project-shade:3900' });
```
**Python / Go / Rust:** generate a client from the OpenAPI spec with `openapi-generator` , or implement the wire protocol directly (8 endpoints, Ed25519 signatures documented in the spec).
**Android:** use the `shade-android` Kotlin module. Same wire protocol, verified by cross-platform test vectors.
## Building locally
```bash
bun run build:docker # build shade-prekey:dev
bun run build:docker -- --tag v1.0.0 # custom tag
GITEA_TOKEN=... bun run publish:docker # build + push to registry
```
## CI publishing
Tag a release and CI publishes automatically:
```bash
git tag v1.0.0
git push --tags
```
`.gitea/workflows/docker.yml` runs tests, builds the image, and pushes both `v1.0.0` and `latest` tags to `gt.zyon.no/stian/shade-prekey` .