Skip to main content

Cloud server & deploy

midnight-manual-server is the corpus host. Most people never run it; they use the hosted instance. It is a single self-contained binary if you want your own.

Architecture​

  • Stack: axum + tower over PostgreSQL 16 with the pgvector extension. An HNSW index powers vector search; a GIN index powers full-text.
  • API surface: anonymous read endpoints (/v1/search, /v1/embeddings proxy, /v1/facets, /v1/chunks/{id} + batch variants + /next//prev//parents, /v1/documents/{id} + /chunks, /v1/sources + /{slug} + /versions, /v1/models/active, /v1/me) and authenticated admin endpoints (the ingest-run protocol, version promote/retire, rate-limit and token-limit management).
  • Auth: Ed25519 challenge-response for admin principals; GitHub OAuth for read-uplift tokens; HS256 JWTs signed with MIDNIGHT_MANUAL_JWT_SECRET.
  • Tiered rate limiting. Anonymous traffic is limited per-IP. Signing in via GitHub OAuth raises the limit for 30 days. Admins can add per-CIDR overrides. A tier guard runs before the role guard, so a read-uplift token can never gain write access.
  • Health endpoints: /healthz (liveness) and /readyz (readiness after the DB pool and model registry are loaded). Request-ID propagation on every request for traceability.
  • Image: Multi-stage Docker build onto gcr.io/distroless/cc-debian12 (no shell, no toolchain), built for linux/amd64 and linux/arm64. Server deploys are always operator-run, not wired into CI.

Running locally against Postgres​

export DATABASE_URL=postgres://localhost/midnight_manual
export MIDNIGHT_MANUAL_USER_STORE=./users.toml
export MIDNIGHT_MANUAL_JWT_SECRET=… # HS256 signing secret, ≥ 32 bytes
cargo run --release -p midnight-manual-server

The server runs automatic migrations at boot when MIDNIGHT_MANUAL_AUTO_MIGRATE=true (the default in fly.toml). Migrations live in crates/mnm-store/migrations/.

For prerequisites and the full list of binaries and Cargo features, see Building from source.

Fly.io provisioning runbook​

The fly.toml at the repo root configures the app for Fly.io deployment. Primary region is lhr (London). The server image is built from Dockerfile.server.

Server deploys are always operator-run (flyctl deploy). They are intentionally not wired into CI. The release pipeline (release-plz) publishes crates and CLI binaries but does not touch the server image.

Prerequisites​

brew install flyctl gh
flyctl auth login
gh auth login

You also need: write access to the GitHub repo (for OAuth App configuration), a Fly.io org with billing, and a VoyageAI account.

1. Create the Fly app​

flyctl apps create midnight-manual --org <your-org-slug>

The app name midnight-manual matches fly.toml. Use a different name only if you also edit fly.toml.

2. Provision Managed Postgres + pgvector​

Use Fly Managed Postgres (fly mpg). The --pgvector flag enables the extension for you:

flyctl mpg create \
--name midnight-manual-pg \
--org <your-org-slug> \
--region lhr \
--plan basic \
--volume-size 10 \
--pgvector

Attach the cluster to the app, which sets DATABASE_URL automatically:

flyctl mpg list
flyctl mpg attach <cluster-id> --app midnight-manual

3. Generate the JWT signing secret​

openssl rand -hex 32 | tr -d '\n' | \
flyctl secrets set MIDNIGHT_MANUAL_JWT_SECRET=- --stage --app midnight-manual

--stage queues the secret without restarting machines yet.

4. Author and stage the user-store TOML​

Generate a keypair for your first admin principal:

mnm keys generate --user-id ops-primary

This writes $XDG_CONFIG_HOME/midnight-manual/keys/ops-primary.private (mode 0600) and echoes the public key to stdout. Build user-store.toml:

schema_version = 1

[[users]]
user_id = "ops-primary"
role = "admin"
public_key = "ed25519:<base64 from mnm keys generate>"
created_at = "2026-05-25"

Keep user-store.toml and all .private files out of git. Stage it as a Fly secret:

flyctl secrets set MIDNIGHT_MANUAL_USER_STORE="$(cat user-store.toml)" \
--stage --app midnight-manual

See Users & access for the full user-management reference.

5. Register the GitHub OAuth App​

GitHub OAuth provides read-uplift tokens for members of your configured org. Skip this step if you do not need the read-uplift tier.

  1. Create an OAuth App at https://github.com/settings/applications/new.
  2. Set Authorization callback URL to https://midnight-manual.midnightntwrk.expert/v1/auth/github/callback (match your cert hostname).
flyctl secrets set \
MIDNIGHT_MANUAL_GITHUB_OAUTH_CLIENT_ID=<client-id> \
MIDNIGHT_MANUAL_GITHUB_OAUTH_CLIENT_SECRET=<client-secret> \
MIDNIGHT_MANUAL_GITHUB_OAUTH_REDIRECT_URL=https://midnight-manual.midnightntwrk.expert/v1/auth/github/callback \
MIDNIGHT_MANUAL_GITHUB_ORG=midnight-network \
--stage --app midnight-manual

If any of these four secrets is missing the /v1/auth/github/* endpoints return 503 cleanly. A partial configuration is safe.

6. Set the VoyageAI key​

flyctl secrets set VOYAGE_API_KEY=<voyage-platform-key> \
--stage --app midnight-manual

Without VOYAGE_API_KEY, /v1/embeddings returns 503 and inline reranking degrades to RRF order. The rest of the server still boots and serves reads.

Before pointing real traffic at the proxy, enable zero-retention on the Voyage account whose key the server uses: training disabled, no data retention. Non-BYOK callers' query text flows through this account, so zero-retention is what keeps it from being retained upstream.

7. Deploy​

flyctl deploy --app midnight-manual
flyctl logs --app midnight-manual

Expected boot sequence in the logs: "resolved active embedding model" -> "starting midnight-manual-server" -> migrations applied -> listener bound on :8080.

8. Smoke test​

HOST=https://midnight-manual.midnightntwrk.expert

curl -fs "$HOST/healthz" # → 200
curl -fs "$HOST/readyz" # → 200 once DB + model registry ready

curl -fs "$HOST/v1/models/active" | jq .
# → {"name":"voyage-context-3","revision":1,"dim":1024,"provider":"voyageai",
# "code":{"name":"voyage-code-3","revision":1,"dim":1024,"provider":"voyageai"}}

Environment variable reference​

The following variables configure server behaviour. Secrets should be set via flyctl secrets set; non-secrets belong in fly.toml's [env] block.

VariableSecret?DefaultDescription
DATABASE_URLyes(set by Fly attach)PostgreSQL connection string.
MIDNIGHT_MANUAL_JWT_SECRETyes(required)HS256 signing secret, ≥ 32 bytes.
MIDNIGHT_MANUAL_USER_STOREyes(required)TOML user-store body (server) or file path (CLI).
VOYAGE_API_KEYyes(unset)Voyage platform key. Without it, /v1/embeddings returns 503.
MIDNIGHT_MANUAL_GITHUB_OAUTH_CLIENT_IDyes(unset)GitHub OAuth client ID for read-uplift.
MIDNIGHT_MANUAL_GITHUB_OAUTH_CLIENT_SECRETyes(unset)GitHub OAuth client secret.
MIDNIGHT_MANUAL_GITHUB_OAUTH_REDIRECT_URLyes(unset)OAuth callback URL.
MIDNIGHT_MANUAL_GITHUB_ORGyes(unset)GitHub org whose members get read-uplift.
MIDNIGHT_MANUAL_AUTO_MIGRATEnotrueRun migrations at boot.
MIDNIGHT_MANUAL_VOYAGE_CONTEXT_MODELnovoyage-context-3Contextualized embedding model (general chunks).
MIDNIGHT_MANUAL_VOYAGE_MODELnovoyage-code-3Flat embedding model (code chunks + /v1/embeddings proxy).
MIDNIGHT_MANUAL_VOYAGE_DIMno1024Embedding output dimension.
MIDNIGHT_MANUAL_VOYAGE_DTYPEnofloatEmbedding output dtype.
MIDNIGHT_MANUAL_SERVER_RERANKno(enabled)Set to off to disable inline reranking.
MIDNIGHT_MANUAL_TOKEN_LIMIT_ANON_HOURLYno2000Hourly token budget, anonymous tier.
MIDNIGHT_MANUAL_TOKEN_LIMIT_ANON_DAILYno20000Daily token budget, anonymous tier.
MIDNIGHT_MANUAL_TOKEN_LIMIT_UPLIFT_HOURLYno4000Hourly token budget, read-uplift tier.
MIDNIGHT_MANUAL_TOKEN_LIMIT_UPLIFT_DAILYno40000Daily token budget, read-uplift tier.
MIDNIGHT_MANUAL_TOKEN_LIMIT_ADMIN_HOURLYno500000Hourly token budget, admin tier.
MIDNIGHT_MANUAL_TOKEN_LIMIT_ADMIN_DAILYno100000000Daily token budget, admin tier.
MIDNIGHT_MANUAL_TOKEN_LIMIT_GLOBALno10000000Site-wide token ceiling (non-admin tiers). u64::MAX disables it.
MIDNIGHT_MANUAL_TOKEN_LIMIT_GLOBAL_WINDOW_SECSno10800Rolling window for the global ceiling (3 h).
MIDNIGHT_MANUAL_TOKEN_SNAPSHOT_SECSno300Interval for flushing token-usage counters (5 min).
MIDNIGHT_MANUAL_SCORING_POLICYyes (optional)(compiled defaults)Custom confidence-scoring policy TOML.
RUST_LOGnoinfoLog level.

Troubleshooting​

SymptomLikely cause
503 on /v1/auth/challenge or /v1/auth/verifyMIDNIGHT_MANUAL_USER_STORE or MIDNIGHT_MANUAL_JWT_SECRET unset.
503 on /v1/auth/github/*Any of the four MIDNIGHT_MANUAL_GITHUB_* secrets missing.
409 embedding_model_mismatch from a CLIClient embedding-model id does not match the active corpus model. Run mnm models active to see the corpus's active wire id, then re-run with --embedding-model <wire-id> (or --embedding-model auto); use mnm models migrate to realign every source in bulk.
relation "chunk" does not exist in logspgvector extension missing or MIDNIGHT_MANUAL_AUTO_MIGRATE=false and migrations not applied.
failed to resolve active embedding model at bootembedding_model table empty: migration 0006_seed_embedding_model.sql did not apply.