formspec serve — Production Single Server
formspec serve menjalankan engine pada mode production: satu proses, satu deployment, tanpa Control Plane. Ia binary yang sama dengan formspec dev — yang berbeda adalah mode dan penegakan batasnya.
Untuk pengembangan sehari-hari gunakan formspec dev; dokumen ini tentang apa yang berubah saat menjalankannya untuk pengguna nyata.
formspec serve --mode=production \
--spec ./spec \
--dsn "postgres://formspec:secret@localhost:5432/app" \
--jwt-secret "…" \
--cors-origin https://app.example.com \
--tls-cert cert.pem --tls-key key.pem \
--metrics-addr 127.0.0.1:9102 --admin-secret "$ADMIN_SECRET"Flag
| Flag | Default | Keterangan |
|---|---|---|
--mode | production | Satu-satunya mode yang diimplementasikan. Nilai lain → exit 2 (pesan mengarahkan ke dev). |
--spec | ./spec | Direktori manifest. Dibaca sekali saat boot — tidak ada watcher. |
--dsn | — | Wajib. Postgres. DSN sqlite: ditolak. |
--addr | :8080 | Alamat REST API + UI. |
--workspace | (lihat di bawah) | Workspace aktif. Berpengaruh pada penegakan uses, OAuth default, dynamic subscription. |
--jwt-secret | — | HS256 shared secret. Salah satu dari ini atau --jwt-public-key wajib. |
--jwt-public-key | — | PEM berisi public key RSA/ECDSA → RS256/ES256. |
--jwt-issuer | formspec | Issuer yang diharapkan pada token. |
--tls-cert/--tls-key | — | Sepasang sertifikat + key → HTTPS (min TLS 1.2). Harus diberikan bersama. |
--cors-origin | — | Wajib, bisa diulang. * ditolak. |
--web-dir | (auto-detect) | Direktori SPA hasil build. Kosong → urutan resolusi di bawah. |
--sidecar-endpoint | — | Endpoint app-process untuk impl: {type: sidecar} (unix://… atau http://…). |
--invoke-timeout | 30s | Batas waktu satu invoke sidecar. |
--metrics-addr | 127.0.0.1:9102 | Listener admin (/metrics, /health, /workspaces). Kosong → dimatikan. |
--admin-secret | — | Bearer token untuk endpoint tulis /workspaces. Kosong → endpoint itu tidak di-mount. |
--log-level | info | debug | info | warn | error. |
--log-debug | false | Mengaktifkan record debug (kontrol operator — wajib dicatat di audit trail operator). |
Batas production yang ditegakkan
serve berhenti di boot dengan pesan yang menyebut todo-nya, bukan diam-diam melanjutkan dengan konfigurasi yang tidak aman:
| Gate | Perilaku |
|---|---|
| SQLite sebagai DSN | ditolak — production wajib Postgres (Fase 8.1.4) |
| JWT tidak dikonfigurasi | ditolak — tidak ada dev auth / identitas sintetis (8.1.2) |
CORS allow-list kosong, atau * | ditolak (8.1.5) |
--tls-cert tanpa --tls-key | ditolak (8.1.3) |
--mode selain production | exit 2 |
Selain itu ia mematikan seluruh jalan pintas dev: tidak ada seeding, tidak ada auto-approve, uses enforcement ketat, dan root redirect GET / → /{ws}/ tidak dipasang (segmen root milik edge: subdomain per workspace atau aturan ingress).
Aset SPA
serve menyajikan UI dari sumber pertama yang ada:
--web-dir <dir>renderers/react-shadcn/dist— di-auto-detect dari CWD ke atas- cache
formspec spa install(per versi binary) - SPA yang ter-embed di binary (
-tags formspec_spa)
Deployment dari checkout repo karena itu tidak butuh flag apa pun. Bila tidak ada satu pun yang ditemukan, banner mencatat no SPA assets — API only (sebuah 404 senyap akan menyamarkan bedanya "flag kurang" dan "build kurang").
Penyajian aset memakai HTTP caching + kompresi yang sama dengan dev (fingerprinted → immutable setahun; sisanya no-cache + ETag; preferensi br → gzip → identity dari sidecar yang ditulis formspec spa compress).
Satu bundle per proses. serve tidak mendukung SPA yang berbeda per workspace: bundle-nya satu untuk seluruh proses, dan SPA membaca slug dari URL. Yang bisa berbeda per App adalah data — tema (App.spec.theme_ref), logo/favicon, app_renderer/chrome, dan komponen asset dari module. Untuk front-end yang benar-benar berbeda per workspace, jalankan satu deployment per workspace.
Runtime action
impl.type | Di serve |
|---|---|
script | Berjalan (Starlark sandbox). |
native | Berjalan bila handler-nya terdaftar di binary (App.RegisterNative). |
sidecar | Berjalan bila --sidecar-endpoint di-set; tanpa itu gagal dengan pesan jelas. |
Listener ctx.* untuk app process non-Go (--listen/--app-endpoint) dan spawning app child process belum ada di serve — keduanya hanya di formspec dev. Perencanaan: docs_internal/plan/serve-parity.md.
Listener admin
--metrics-addr menyajikan:
| Endpoint | Isi |
|---|---|
GET /metrics | Prometheus (todo 8.2.4, 09-observability.md §3.1) |
GET /health | {status, reasons, checked_at} (8.2.6); 503 saat unhealthy |
GET /workspaces | Daftar workspace terdaftar — hanya bila --admin-secret |
POST /workspaces | {"slug":"kopi","name":"Kopi Kita"} → daftarkan |
DELETE /workspaces/{slug} | Cabut registrasi (soft; data tenant tidak disentuh) |
Default bind-nya loopback (127.0.0.1:9102) karena listener ini tidak memiliki autentikasi. /metrics dan /health dijangkau operator lewat loopback atau port-forward; mengeksposnya ke jaringan (mis. --metrics-addr :9102) harus keputusan sadar. Tanpa --admin-secret, endpoint /workspacestidak di-mount sama sekali — bukan menjawab 401, melainkan tidak ada — sehingga tidak ada permukaan tulis untuk diserang.
Menambah / menghapus workspace tanpa restart
Slug URL workspace divalidasi per request terhadap registry di database (formspec.core/workspace). Karena itu workspace yang baru didaftarkan langsung routable pada request berikutnya — tidak ada proses restart, tidak ada cache yang perlu dibuang:
curl -H "Authorization: Bearer $ADMIN_SECRET" \
-d '{"slug":"cabang2","name":"Cabang 2"}' \
http://127.0.0.1:9102/workspaces
# → 201 {"slug":"cabang2","created":true}
curl http://127.0.0.1:8080/cabang2/_ui/_meta/apps # sudah 200, tanpa restartAdmin pertama untuk workspace baru dibuat lewat wizard setup pemilik workspace itu: POST /{ws}/_ui/setup (hanya berlaku selama workspace belum punya user).
Aturan yang perlu diketahui:
- Slug divalidasi dengan aturan loader manifest yang sama: kebab-case, bukan segmen reserved (
_ui,api,_admin,assets,health,login,register,_ws,print). - Mendaftarkan slug yang sudah ada bersifat idempoten (
created: false, display name diperbarui) — sumber deklaratif (kind: Workspace) dan jalur runtime bertemu di baris yang sama. DELETEhanya mencabut registrasi; baris data tenant tetap dan bisa dibaca lagi bila slug didaftarkan ulang. Menghapus data tenant adalah operasi tersendiri (formspec backup), bukan efek samping perubahan routing.- Workspace
defaulttidak bisa dihapus: ia fallback CLI/SPA dan di-seed ulang setiap boot. - Menghapus manifest
kind: Workspacetidak mencabut registrasi (syncWorkspaceRegistryhanya meng-Ensure, tidak pernah menghapus).
Batas yang disadari
| Batas | Alasan / jalur keluar |
|---|---|
| Satu DSN per proses | Semua workspace berbagi satu database, dipisah tenant_id. Isolasi fisik = satu deployment per workspace. kind: Datastore + access.filter.workspaces ada, tetapi binding snapshot (level 3) belum ter-wire di single-server. |
| Tidak ada hot-reload spec | Manifest dibaca saat boot. Perubahan manifest = restart. |
| Kredensial datastore lewat env | credential_ref (kms://…) dideklarasikan di spec tetapi belum ada resolver KMS/Vault. |
| Storage fallback = filesystem | Tanpa kind: Datastore object store, ctx.storage menulis ke <project-root>/.formspec/storage. Di container itu ephemeral — pakai object store atau volume persisten. |
| Migrasi destruktif menolak boot | Gerbang sengaja. Jalankan formspec migrate apply lebih dulu (atau formspec repl --no-sync -f repair.star untuk repair). |
| SPA berbeda per workspace | Satu bundle per proses (lihat "Aset SPA"). |
| App process non-Go | Belum ada listener ctx.* / spawning child process di serve. |
Sisa pekerjaan terbuka: docs_internal/plan/serve-parity.md dan item [⏸️] di Fase 8 docs_internal/plan/todo.md.