Workspace, App, Module
Version: 0.1.0 · Status: Draft
Draft: isi di bawah kontrak yang berlaku. Bagian yang masih terbuka ditandai eksplisit sebagai Open.
1. Model Kepemilikan
Workspace → App → Module → ResourceSatu workspace berisi banyak App dan banyak Module. Module memiliki objek (Entity, Service, dan seluruh instance VisualSpecKind — Page, Form, Table, dst.) — satu Module = satu bounded context bisnis utuh, tidak dipecah jadi "module backend" vs "module frontend" (field/schema, layout form, list view, permission berubah bersamaan; memisahkannya menambah overhead deklarasi lintas-module yang tidak proporsional untuk sesuatu yang sebenarnya satu unit). App adalah kurasi — keranjang objek dari module-module yang dideklarasikan lewat depends_on (../backend/01-core-basic.md §5), bukan pemilik objek. Satu Module yang sama boleh di-mount lebih dari satu App dalam workspace yang sama, masing-masing meng-expose subset berbeda (App internal vs App publik yang mengakses data sama).
Workspace adalah satu-satunya model multi-tenancy FormSpec. Aplikasi ditulis sepenuhnya tenancy-blind — tidak ada kode aplikasi yang memilih strategi tenancy, dan setiap Entity tenant-isolated secara default, tanpa pengecualian; satu workspace = satu tenant terisolasi. Tidak ada akses lintas-workspace dalam bentuk apa pun di dalam framework — kalau integrasi lintas-workspace suatu saat dibutuhkan, itu hidup di level external service, di luar FormSpec Framework. Data characteristic: reference dimiliki App Owner (di-seed lewat rilis, read-only bagi Data Owner; backend juga mendukung find-or-create otomatis saat pertama kali diakses — lihat backend/01-core-basic.md §1.1); seluruh data tenant-isolated lainnya dimiliki Data Owner. Strategi dan topologi isolasi (single vs multi, pooled vs isolated, tiering) bukan urusan spec aplikasi — diputuskan saat deployment oleh Platform Operator (04-control-plane.md §2). Tenant besar yang ingin server sendiri memakai lisensi enterprise dan menjalankan FormSpec Cloud-nya sendiri sebagai Platform Operator, bukan lewat mode tenancy khusus di dalam aplikasi.
1.1 Workspace Registry — slug bernama
Slug workspace adalah segmen pertama URL (/{ws}/...) dan sekaligus workspace ID — tidak ada mapping slug→UUID; semua store, session, dan permission di-scope langsung oleh slug. Slug wajib kebab-case dan tidak boleh memakai segmen reserved router (_ui, api, _admin, assets, health, login, register, _ws, print).
Workspace harus terdaftar di workspace registry (entity bawaan formspec.core/workspace — fields name, slug unique, owner_user_id, settings). Ada dua sumber yang konvergen ke registry yang sama:
Manifest
kind: Workspace— seed deklaratif, di-upsert ke registry saat boot dan hot-reload:yamlapiVersion: formspec.dev/v1 kind: Workspace metadata: name: cafe spec: display_name: "Kafe Demo"(
spec.slugopsional; defaultmetadata.name.)Manifest ini mendaftarkan, bukan memilih. Workspace yang aktif saat runtime ditentukan oleh
--workspace-id(atau config file) — manifest hanya memastikan slug-nya ada di registry. Salah membaca ini membuat data masuk ke tenant yang tidak diduga: mendeklarasikancafe, menjalankan dev tanpa flag, dan semua tulisan tersimpan dengantenant_id: "default"sementaraGET /cafe/...menjawab200dengan nol baris (terlihat sehat, isinya beda).formspec devmenutup kebingungan itu di kedua arah: kalau tree-nya mendeklarasikan tepat satu workspace, dev memakainya dan mengumumkannya; kalau lebih dari satu, ia tetap memakaidefaultdan memperingatkan sambil menyebut daftarnya; dan--workspace-idyang tidak dideklarasikan juga diperingatkan.CLI — penambahan runtime:
formspec workspace create <slug> --name "..." --dsn <dsn>(jugalist,delete --confirm).
WorkspaceMiddleware memvalidasi setiap slug URL terhadap registry: slug tak terdaftar → 404 WORKSPACE_NOT_FOUND (anti-enumeration, konsisten dengan cross-workspace check di §AuthMiddleware). Workspace default selalu di-seed otomatis saat boot agar fallback URL tanpa slug tetap routable. Proses tanpa registry ter-wire (embedded/test) bersifat pass-through — validasi hanya aktif saat registry terpasang.
2. Module
Package manifest — identitas, versi, dependency. Isi ditemukan lewat scanning file, bukan didaftar manual (metadata.name = permission namespace). Struktur di dalamnya adalah closed set: Document, Service, instance VisualSpecKind (Page/Form/Table/dst.), dan deklarasi permission yang mengikat semuanya — bukan tipe bebas. Module tidak wajib mengisi semua jenis itu; Module murni integrasi (mis. formspec/tax-calculator) boleh cuma berisi Service.
apiVersion: formspec.dev/v1
kind: Module
metadata:
name: general-ledger
spec:
version: 1.2.0
vendor: acme-corp
depends:
- module: formspec/core
- module: billing
version: ">=1.0 <2.0"
datastore:
default # opsional (legacy) — nama kind: Datastore untuk ctx.db()
# module ini; setara datastores: {db: <name>}. Lihat
# ../backend/01-core-basic.md §3 dan 06-datastore.md §1.1
datastores:
db: pg-main # opsional — seleksi per primitive (06-datastore.md §1.1);
# key "primitive" = default service, "primitive/alias" = named
# logical primitive (ctx.db.named("alias"))
config:
fiscal_year_start: "04-01" # module-specific: GL mulai tahun fiskal April
coa_structure: "4-2-2-2" # module-specific: struktur kode akun 4 segmen
# Currency, locale, timezone → settings.* (global), bukan di sini
# Lihat ../backend/01-core-basic.md §10
menu: [] # default menu suggestion, module-relative — lihat §4
ai_index: # opsional — metadata discovery untuk FormSpec AI, lihat
# ../../ai/04-formspec-remote-mcp.md §3
category: payment
features: [charge, refund, webhook_callback]
integration_pattern: |
depends: [{module: payment-gateway-xendit}]
skills_for_ai: |
Pakai module ini kalau bisnis butuh terima pembayaran online.2.1 Identitas Unik & Alias saat Konflik Nama (Module Vendor)
metadata.name yang ditulis pembuat module (mis. billing) tidak dijamin unik secara global — dua vendor berbeda boleh memilih nama yang sama. Identitas unik sesungguhnya ada di source module (mis. github.com/acme/billing-module), dicatat di formspec.lock (08-project-layout.md §6.2). Ini hanya berlaku untuk module yang diinstal lewat formspec module install (07-marketplace.md §3) — Module lokal hand-authored tetap satu-satunya pemilik metadata.name-nya sendiri, tanpa alias.
Saat instalasi bentrok dengan nama efektif module lain yang sudah pernah diinstal (aktif maupun masih nonaktif), installer otomatis memberi alias, dicatat sebagai blok marker di manifest App (08-project-layout.md §6.3):
spec:
modules:
- billing # module lokal
# >>> formspec:vendor github.com/acme/billing-module @1.0.0
# - acme-billing
# <<< formspec:vendorUncomment - acme-billing mengaktifkannya — bentuknya tetap string biasa, konsisten dengan elemen App.spec.modules lain (§3). Source dan versi asal tercatat di baris marker >>> dan di formspec.lock (08-project-layout.md §6.2), bukan di bentuk entri itu sendiri.
Alias dihitung saat install, bukan saat aktivasi — nama efektif module tidak boleh berubah tergantung urutan aktivasi developer: kalau dua vendor dengan nama sama sama-sama diaktifkan kapan pun kemudian, tidak boleh ada surprise rename. Konsisten dengan prinsip gap-free yang sama dipegang di tempat lain di spec (mis. ctx.next_key, ../backend/01-core-basic.md) — nomor/nama tidak boleh berubah makna tergantung state runtime.
Enforcement saat boot: formspec-server mengecek nama efektif (alias kalau ada, metadata.name kalau tidak) hanya untuk set module yang aktif. Bentrok di set aktif → refuse to boot dengan pesan jelas, minta alias manual. Module yang belum diaktifkan tidak pernah dicek — dua vendor module bernama sama boleh nangkring bersamaan di vendors/ selama tidak dua-duanya aktif tanpa alias.
3. App
Root project manifest — unit deployment, trust boundary, dan publikasi interface. Satu workspace boleh berisi lebih dari satu App; seluruh App di satu workspace berjalan bersamaan dalam proses yang sama, dibedakan root_url.
apiVersion: formspec.dev/v1
kind: App
metadata:
name: klinik-sehat-internal
spec:
version: 2.1.0 # optional — marketplace publishing metadata
vendor: acme-corp # optional — marketplace publishing metadata
root_url: /app/klinik-internal # prefix mount bebas di dalam workspace (mis. "/", "/barbershop") — wajib unik per App dalam satu workspace
workspaces: [klinik] # optional — allowlist mount workspace (lihat §3.1)
modules: [billing, acme-corp/general-ledger]
app_renderer: sidebar-nav # pilih App renderer — lihat spec/frontend/05-app-kinds.md
menu: [] # lihat §4
registered_views: [] # permukaan tambahan di luar menu — lihat frontend/05-app-kinds.md §1.2
publishes: # interface lintas-app yang ditawarkan
- service: icd-lookup
actions: [search, find]
consumes: # interface lintas-app yang dibutuhkan → memicu grant request
- app: bpjs-gateway
service: claims
actions: [submit-claim]Default private. Akses lintas-app hanya lewat publish → request → grant disetujui Data Owner, tercatat, revocable, metered (04-control-plane.md §5 Contracts).
3.1 workspaces — Allowlist Mount Workspace
Field opsional spec.workspaces membatasi di workspace mana App di-mount (plan docs_internal/plan/named-workspaces.md). Pointer semantics — tiga state yang dibedakan eksplisit:
| Deklarasi | Efek |
|---|---|
| field tidak ada (default) | App di-mount di semua workspace deployment — backward compatible |
workspaces: [] (eksplisit kosong) | Staged — App tervalidasi & ter-reload, tapi di-mount di mana pun (App di-upload tapi belum di-bind; formspec check memberi warning) |
workspaces: [cafe, kopi] | Hanya workspace tersebut |
Aturan: slug format-only (kebab-case, bukan segmen reserved) — slug boleh mereferensikan workspace yang dibuat belakangan via CLI; keberadaan registry tidak divalidasi saat manifest load. Enforcement request-time: /_meta/apps hanya menampilkan App yang di-mount di workspace request, app-scoped /_meta/ui menolak App di luar allowlist, dan SPA mount root_url-nya 404 — semuanya 404, bukan 403 (anti-enumeration, konsisten §15.2).
Limitasi: anonymous surface App access: public (registrasi permission entitas publiknya) masih berlaku global, tidak per-workspace — deferred.
Versi: version/vendor adalah metadata publikasi (opsional, semver bila diisi) — diekspos di /_meta/apps untuk inspeksi. Per-workspace App versioning BUKAN konsep in-process: satu proses selalu menjalankan satu versi; upgrade per workspace = keputusan control plane (artifact registry vendor/app@version + binding state per deployment — cloud phase, 07-marketplace.md).
app_renderer memilih archetype chrome App ini (../frontend/01-visual-hierarchy.md, ../frontend/05-app-kinds.md) — nilainya nama Renderer terdaftar (sidebar-nav, topnav, no-nav, dst), bukan enum tertutup di level kontrak App (Renderer baru bisa didaftarkan kapan saja tanpa mengubah skema App). Auth dipisah di access (private/public); stack_family memilih shell implementasi; persist_backend memilih backend persist entity.
Theme adalah app-specific (normatif). Theme di-resolve di level App lewat field theme_ref di App.spec — bukan per workspace. Alasannya: beda App bisa memakai Shell berbeda, dan bahkan dua App di Shell yang sama bisa punya kebutuhan brand berbeda (App internal vs App publik satu vendor). Workspace boleh menetapkan Theme default sebagai fallback untuk App yang tidak mendeklarasikan theme_ref, tapi keputusan akhir selalu di manifest App. (../frontend/05-app-kinds.md §6 mengikuti keputusan ini.)
Auth & authorization per-App (normatif). Autentikasi dan otorisasi di-resolve di level App, bukan enum tertutup:
- Authentication — App mendeklarasikan skenario auth yang dipakainya lewat konfigurasi auth (
auth_config_ref), memilih dari strategy yang terpasang:basic-auth,sso(OIDC/SAML),social-sso(Google, Facebook, GitHub, dst),passwordless(magic link/OTP),passkey(WebAuthn), dan seterusnya. Set strategy terbuka untuk ditambah (bukan closed enum) — strategy baru didaftarkan sebagai artifact, mengikuti trust tier yang sama dengan artifact lain. - Authorization — dievaluasi di level permission module (
{module}.{entity}.{action}, mis.invoice.create) dan boleh juga memeriksa atribut App, user, membership, atau konteks lain (attribute-based) di samping pengecekan permission berbasis role.
"FormSpec-ID" bukan primitive tersendiri (normatif). Konsep identitas lintas-workspace dengan consent ledger portable (identitas manusia yang dikenali lintas banyak workspace, membawa riwayat consent) di luar scope FormSpec Framework — bertentangan langsung dengan prinsip tenancy §1: workspace adalah satu-satunya batas isolasi, tanpa akses lintas-workspace dalam bentuk apa pun. Kalau kebutuhan semacam itu muncul, tempatnya di level external service (../backend/01-core-basic.md §3), bukan diselesaikan sebagai fitur FormSpec Framework.
Yang tetap relevan dan tidak butuh konsep baru: FormSpec Cloud boleh menawarkan server OIDC/OAuth terkelola sebagai kenyamanan infra — supaya App Owner tidak perlu memasang identity provider sendiri — persis seperti menawarkan Postgres terkelola. Ini cukup jadi satu pilihan di bawah strategy sso yang sudah ada di atas, dikonfigurasi lewat auth_config_ref seperti provider OIDC lain mana pun — tanpa nama/branding atau kontrak tersendiri.
Multi-cabang (branch) — pola yang direkomendasikan. Cabang bukan alasan membuat App terpisah per cabang (App per cabang menduplikasi kurasi dan menu hanya demi satu kode pembeda). Cabang adalah scoping data: entity yang branch-aware memakai field cabang (di-scope lewat scope_field pada natural key bila perlu nomor urut per cabang), membership user membawa atribut cabang, dan authorization attribute-based (di atas) mengevaluasi atribut itu — satu App, banyak cabang. Kalau satu cabang butuh kurasi UI yang benar-benar berbeda, barulah App terpisah dipertimbangkan — sebagai keputusan kurasi, bukan keharusan teknis.
4. Menu
Menu milik App, independen dari Module — bukan keputusan estetika, konsekuensi langsung dari fakta bahwa View/Action yang di-expose bisa berbeda per App-mount (App publik cuma expose wizard+cek-status, App internal expose list+approve dari Module yang sama). Menu — enumerasi "apa yang bisa dicapai lewat navigasi" — harus ditentukan di level yang sama dengan keputusan visibility itu, yaitu App. Analogi: Module = katalog, App.menu = daftar belanja dari katalog itu.
App.spec.menu dan Module.spec.menu sama-sama []MenuItem — array = App.spec.menu (otoritatif), atau saran default Module.spec.menu yang diadopsi App lewat type: module Adopt node.
Urutan item di list = urutan tampil (tidak ada field order terpisah). Supaya App Owner tidak dibebani wiring manual dari nol, Module boleh menyediakan default menu suggestion (Module.spec.menu) yang bisa langsung diadopsi App lewat Adopt node — App tetap bebas override/restrict/rearrange.
MenuItem (dipakai identik di App.spec.menu dan Module.spec.menu):
type MenuItem struct {
Type string // "module" = adopt-shorthand node; kosong = grup/leaf biasa
Label string
Icon string
Module string // wajib di leaf & node type:module; terlarang di grup
View string // nama View terdaftar (Page/Table/Wizard/Kanban/Dashboard/Report/Timeline)
Route string // escape hatch: URL mentah untuk leaf tanpa View terdaftar
When string // kondisi bisnis FormSpecExpr
Children []MenuItem
}Nesting dibatasi 3 level; tiap node wajib salah satu dari tiga bentuk (divalidasi saat load, bukan diam-diam dipaksa jadi bentuk lain):
- Adopt node (
type: module, level 1 saja) — wajibmodule; menyisipkan seluruh menu suggestion Module itu di posisi ini. - Group node (punya
children, level 1/2) — wajiblabel+childrentak kosong; terlarangmodule/view/routedi node itu sendiri (grup boleh berisi children dari module berbeda). - Leaf/action node (tanpa
children, bukantype: module, level 2/3) — wajiblabel,module, dan tepat satu dariview/route. Leaf level-3 tidak boleh punyachildren— ini yang menegakkan batas 3 level.
Resolusi route: leaf ber-view me-resolve route dari registrasi View itu sendiri (Page pakai route:-nya; Dashboard/Widget/Wizard/Kanban/Timeline/ Report/Print/Calendar/Listing/ApprovalInbox/NotificationCenter pakai konvensi /<kind-lowercase>/<name>) — route tidak pernah diduplikasi ke item menu supaya tidak bisa drift.
Form dan Table boleh jadi target view: keduanya mendapat derived Page wrapper dengan route /<module>/form/<name> dan /<module>/table/<name> (kecuali public: false), dan route itu juga dipakai footprint grant ({entity}-page, ../../runtimes/). Wrapper-nya dibuat tanpa mode, sehingga halaman itu selalu merender mode view — untuk create/edit gunakan blok form: di sebuah Page dengan mode eksplisit.
Tidak ada kind: Menu standalone — sudah dilebur seluruhnya ke App.spec.menu (otoritatif) dan Module.spec.menu (saran default).
Menu adalah deklarasi permukaan, bersama registered_views. Permukaan App yang dapat diakses adalah setiap target leaf menu ∪ registered_views — menu yang dipakai (view: maupun route:) mendaftarkan targetnya sendiri, dan registered_views menambah view yang tidak dijangkau navigasi (khususnya App tanpa menu). Route di luar himpunan itu tidak didaftarkan (404). Batasan, tiga state, dan exemption _admin/grants ada di ../frontend/05-app-kinds.md §1.2.
Visibilitas item menu — dua sumbu
MenuItem membawa dua field yang tidak boleh saling menggantikan:
| Field | Sumbu | Dievaluasi | Bypass-able |
|---|---|---|---|
permissions | RBAC — sembunyikan dari pemanggil yang tidak memegang salah satunya (any-of) | server, saat bundle dibangun | tidak — item tidak pernah terkirim |
when | kondisi bisnis (FormSpecExpr) — sembunyikan saat false | klien | ya, memang |
when bukan gerbang otorisasi dan tidak boleh dipakai begitu: menyembunyikan tautan tidak memberi atau menolak akses — route tetap ada dan datanya tetap dijaga required_permission di resource serta filter visibilitas entity di bundle. when dievaluasi klien karena kondisinya bisa bergantung waktu (today()), sedangkan bundle /_meta/ui di-cache lewat ETag. Untuk membatasi siapa yang boleh melihat/membuka sesuatu, pakai permissions.
Konsekuensi yang perlu diketahui: setiap bundle kini App-scoped — surface unscoped _admin (?admin=true) sudah dipensiunkan, jadi permissions/when selalu berlaku dan tidak ada lagi gerbang biner yang membuka seluruh module (docs_internal/plan/app-scoped-login.md D4).
Grammar when mengikuti ../frontend/08-formspec-expr.md, termasuk himpunan callable tertutupnya; formspec check menolak ekspresi di luar itu saat deploy. Konteks evaluasinya { user } (identitas pemanggil) — bukan permission; lihat dokumen itu §3.
5. Qualifier Referensi Antar Module
Notasi module/resource untuk referensi lintas module — konsisten dengan sources.resource, penamaan named script {module}/{script-name} (../backend/02-core-extended.md §7), dan qualifier entity di menu App multi-module. Referensi di dalam module sendiri tanpa qualifier (resource: invoice, konteks sudah jelas satu module) — analog package-qualified reference di Go (Invoice dalam package sendiri vs billing.Invoice dari luar).
6. Validasi formspec apply
- Setiap
moduleyang direferensikan di manapun dalamApp.spec.menu(leaf atau adopt node) wajib anggotaApp.spec.modules. root_urlwajib unik lintas seluruh App dalam satu workspace dan diawali/app/.
7. Akses Lintas-Module
Tiga jenis interaksi, level coupling berbeda — disarankan urutan preferensi dari longgar ke erat: event subscribe (paling longgar, async, tanpa dependency waktu-boot — kind: Subscription, ../backend/01-core-basic.md §7), action/ service call (A cukup tahu kontrak Action, tidak tahu skema internal B), entity read langsung (paling erat — dibatasi untuk kasus read-only sederhana, mis. cek data referensi). Framework tidak melarang entity read lintas-module, tapi konvensi mengarahkan ke pola lebih longgar untuk apa pun yang menyangkut behavior, bukan sekadar baca.
Kalau kedua Module resolve ke service fisik berbeda (spec.datastore / spec.datastores, §2), preferensi di atas jadi keharusan (normatif):entity read langsung dan action/service call lewat ctx.db sama-sama tidak tersedia lintas-Datastore — satu-satunya jalur yang tersisa adalah event subscribe (../backend/01-core-basic.md §3, 06-datastore.md §1.1). Beda service fisik = beda deployment boundary; tidak ada tingkat consent yang membuka akses langsung ke sana.
Deklarasi dependency di level Module (spec.depends, §2) memberi visibilitas dependency graph untuk tooling registry/marketplace — bukan cuma untuk enforcement runtime.
Consent lintas Module Owner berbeda. Akses antar-Module dengan Module Owner yang sama cukup lewat depends_on + deklarasi permission biasa. Akses antar-Module dengan Module Owner berbeda (mis. workspace menginstal module dari dua vendor marketplace berbeda, keduanya perlu saling baca data) butuh consent eksplisit dari Workspace Owner — bukan dari Module Owner asal data, konsisten dengan prinsip data adalah milik workspace (lihat 04-control-plane.md §5 soal model contract/ consent). Module Owner cuma menyediakan permukaan publik (subset Entity/ Action yang sengaja di-expose untuk dikonsumsi Module lain); keputusan "boleh dikonsumsi atau tidak" tetap di tangan Workspace Owner.
Bentuk artefak consent (normatif). Consent lintas-Module-Owner memakai flag pada depends_on yang sudah ada (mis. depends_on: [{module: inventory, owner_consent: required}]) dan mendaur ulang alur approval Policy — bukan kind artefak baru (reuse mekanisme, bukan tambah primitive).
Akses lintas-module dalam satu workspace didukung penuh & terverifikasi tooling. Selama masih dalam satu workspace, module boleh saling mengakses sesuai deklarasi depends_on + permission. Kejujuran deklarasi ditegakkan statis oleh formspec check (../../cli-tools/02-formspec-cli.md), yang wajib melaporkan minimal: (a) seluruh unresolved varname di script, (b) akses lintas-module yang belum di-approve (dipakai di kode tapi tak dideklarasikan/di-consent), dan (c) deklarasi lintas-module yang tidak terpakai (declared tapi tak pernah diakses — kandidat dicabut). formspec check --fix memperbaiki yang bisa diperbaiki otomatis (menambah deklarasi yang kurang setelah konfirmasi, menghapus yang tak terpakai).
8. Identitas User & Membership
Identitas user hidup di level workspace — satu akun per manusia, prasyarat untuk grant lintas-app, audit yang konsisten, dan SSO. Membership dan penetapan role bersifat per-App: populasi user tiap App dalam satu workspace boleh sepenuhnya berbeda (App internal dipakai staf, App publik dipakai pelanggan) walau keduanya me-mount Module yang sama. Definisi role tetap dimiliki Module → otomatis ter-scope per-App saat Module itu di-mount. Membership boleh membawa atribut (mis. kode cabang) yang dievaluasi authorization attribute-based (§3). Membership disimpan di formspec.core dan didistribusikan ke Resource Plane lewat snapshot Plane Protocol (04-control-plane.md §1, 05-plane-protocol.md §4.1).
9. Resource Bawaan formspec.core
formspec.core adalah module khusus (special/reserved module) framework — namespace resource yang selalu ada di setiap workspace, terlepas dari Module apa pun yang terpasang. Ia tidak perlu di-depends_on, tidak perlu diinstal, dan tidak boleh dideklarasikan oleh Module user (namespace-nya reserved milik framework). Enumerasi di bawah adalah referensi "apa yang selalu tersedia" bagi developer yang perlu merujuk resource ini (mis. mengueri audit log, mengecek role assignment) tanpa harus memiliki Module sendiri yang mendeklarasikannya.
9.1 Karakteristik khusus
formspec.core berbeda dari Module biasa karena:
- Reserved namespace — user tidak bisa mendeklarasikan module bernama
formspec.core; namespace ini milik framework. - Bundled module (dogfooding) — didefinisikan sebagai bundled module YAML yang di-embed ke binary (
internal/auth/module/,//go:embed) dan dimuat lewat manifest loader — jalur yang sama dengan module user. Entity auth (user,session,role,api-key,app-membership,workspace) diekspresikan sebagai YAML manifests, bukan registrasi programatik Go.formspec generate authmenyalin module ini keexternal/authuntuk dikustomisasi (user override menang). - Special-casing framework — beberapa fitur framework meng-hardcode
formspec.core:- Global settings (
settings.*, spec §10): entityapp-setting(characteristicreference, natural key"global") adalah Configuration Page pattern — backend membaca/men-seed record ini saat find-or-create (mergeRunningSettings+seedSettingsData) dan meng-overlay-nya kebundle.settings. - Auth core —
user,session,role,api-key,app-membership,workspacedi-backing oleh entity di namespace ini.
- Global settings (
- Selalu tersedia — tidak perlu deklarasi
depends_on; setiap module otomatis bisa merujuk resourceformspec.core.
| Resource | Isi | Kaitan |
|---|---|---|
workspace | Identitas dan metadata tenant — unit multi-tenancy tunggal FormSpec | §1 Model Kepemilikan |
user | Akun manusia di level workspace — satu akun per manusia | §8 Identitas User & Membership |
app-membership | Record membership per-App (populasi user + atribut mis. kode cabang, ter-scope per App) | §8 Identitas User & Membership |
role | Definisi role — dimiliki Module, otomatis ter-scope per-App saat Module di-mount | §8 |
api-key | Kredensial akses non-interaktif (service/integrasi) | 04-control-plane.md §5 |
session | Sesi login aktif user | §3 (auth per-App) |
job | Pelacakan async job — mengikat kontrak wire async action (../backend/02-core-extended.md) | — |
audit-log | Jejak audit bisnis append-only, immutable | ../backend/02-core-extended.md §11 Business Audit Trail |
setting | Namespace global-settings settings.* (workspace/App Config) | ../backend/01-core-basic.md §10 Config & Global Settings |
app-setting | Record runtime global-settings (Configuration Page pattern, natural key "global") — running value yang di-overlay ke settings.* | ../backend/01-core-basic.md §10 Config & Global Settings |
Selain resource di atas, formspec.core juga meng-expose service endpoint bawaan health dan metrics untuk observability — kosakata health machine-readable dan set metric Prometheus didefinisikan di 09-observability.md (§5 Kosakata Health, §3 Metrics).
Resource formspec.core mengikuti model kepemilikan data workspace yang sama (§1): data ini milik workspace, bukan milik Module Owner mana pun. Akses tetap tunduk pada permission — merujuk resource ini bukan berarti bebas otorisasi.
9.2 Route & Page yang disediakan
formspec.core menyediakan halaman (Page) eksplisit dan route CRUD derived untuk entity yang di-ui-exposed. Route di-mount ke App yang me-deklarasikan formspec.core di spec.modules (§3).
Page eksplisit:
| Page | Route | Isi |
|---|---|---|
access-management | /access-management | Kelola user & role dalam satu halaman — tabs Users dan Roles |
settings | /settings | Configuration Page — edit global settings (app-setting, natural key "global") |
Entity + derived CRUD route (yang formspec.dev/ui-exposed: "true"):
| Entity | Karakteristik | Derived route (list) |
|---|---|---|
user | master | /users |
role | master | /roles |
api-key | master | /api-keys |
app-membership | transaction | /app-memberships |
Entity workspace dan session bersifat internal (tidak ui-exposed) — dipakai framework, tidak punya route UI.
9.3 Akses dari script
Script (Starlark) mengakses resource formspec.core lewat:
resource.fetch("formspec.core.<entity>", id)— membaca record entity (notasi titikmodule.entity). Contoh:resource.fetch("formspec.core.user", user_id). Akses lintas-module dari hook wajib dideklarasikan diuses.resourcesaction yang menaunginya (../backend/02-core-extended.md§7).resource.find("<module>.<entity>", {field: value, ...})— mencari satu record berdasarkan nilai field (semua pasangan harus cocok, AND), tanpa SQL. Mengembalikan resource atauNone. Ini bentuk yang benar untuk guard keunikan ("apakah baris dengan kunci ini sudah ada?") — pencariannya lewat lapisan entity, jadi tenant isolation &row_scopeberlaku dan nama tabel/kolom fisik tidak perlu diketahui script. Contoh:resource.find("cafe-master.menu-item-price", {"branch_id": b, "menu_item_id": m}). Akses lintas-module tunduk aturanuses.resourcesyang sama sepertiresource.fetch.ctx.config().get("settings.*")— membaca global settings (currency, locale, timezone, date_format, decimal_scale, rounding) — namespacesettings.*di Config (spec §10,../backend/01-core-basic.md§10). Contoh:ctx.config().get("settings.default_currency").ctx.db()— query langsung ke datastore module; akses lintas-module wajib deklarasiuses(../backend/01-core-basic.md§3).
9.4 Override default value
Nilai default formspec.core bisa di-override di beberapa level:
- Runtime settings (global settings) — record
app-setting(natural key"global") adalah running value yang di-overlay kesettings.*saat/meta/uibundle di-serve. Admin mengubah lewat halaman Pengaturan (/settings); field kosong di record fallback ke manifestsettings:(kind: Config). Backend men-seed record dari manifest saat find-or-create pertama kali. - Override module auth (
external/) —formspec generate authmenyalin bundled moduleformspec.corekeexternal/authuntuk dikustomisasi. Entity diexternal/menang atas default bawaan (user override menang). Lihat08-project-layout.md§6. - Shadow copy (
overrides/) —formspec overridemembuat shadow copy per-kind; module+kind+name yang sama dioverrides/menggantikan total. Lihat08-project-layout.md§6.4. auth_config_ref— App bisa mereferensikan Config auth sendiri untuk mengganti strategy/konfigurasi auth defaultformspec.core. Lihat §3.