Reference · App Databases
Per-app Postgres, with the guardrails IT wants by default.
Forigi can provision a dedicated Postgres schema for each app that needs stateful storage. Tables are declared by the bundle author, approved by a tenant admin, and accessed via an SDK that enforces row-level visibility and per-tenant encryption. This is the same flexibility a custom-built app would have, inside the platform's audit and isolation guarantees.
Where the data lives
Each app gets a Postgres schema named app_<uuid_no_dashes> in the platform's Supabase database. Schemas are completely isolated from each other and from platform-internal tables. The schema only exists after a tenant admin explicitly approves it; nothing is auto-applied.
- One schema per app. Apps cannot read each other's tables, even within the same tenant.
- Postgres tables, not document storage. Real indexes, real types, real query planning. Migrations are additive (drop the table to make destructive changes).
- System columns on every row.
_id,_created_at,_updated_at,_created_by_oid(Microsoft Object ID of the row's creator),_visibility(publicorprivate). Stamped server-side; bundles cannot forge them.
Row-level access, by default
Every read is filtered by the viewer's identity. Bundles never see rows the viewer isn't allowed to see, regardless of the query they construct.
- Public rows — visible to any viewer with access to the app.
- Private rows — visible only to the row's creator (matched by
_created_by_oid) and to tenant admins. - App owners and tenant admins see all rows for diagnostics, support, and exports.
- Default visibility is set per-table in
forigi.json; the bundle can override per-row at insert time within the bounds the schema allows.
Sensitive columns at rest
Mark any column sensitive: true in the manifest and Forigi encrypts it at rest using a per-tenant key derived via scrypt from a platform master key. The key is unique per tenant; even with full database access, a Forigi operator cannot read another tenant's sensitive values without compromising both the master key and the tenant's derived key material.
- AES-256-GCM with per-row IV
- Per-tenant key (scrypt KDF over a platform master key)
- Encrypt on write, decrypt only on read by an authorized viewer
- Decryption errors silently surface as
null— the platform refuses to leak ciphertext if a key is missing
Tenant settings IT can flip
App Databases come with seven IT-controlled toggles. Default posture is conservative — the master switch is OFF. Your IT admin opts in by tenant in /admin/settings.
Master switch. When OFF, the SDK's window.platform.db calls return APP_DATABASES_DISABLED. Default OFF.
When ON, schema inference can call Anthropic with the bundle's source code (never data rows). Default OFF; manifests required.
When OFF, only app owners and tenant admins can write. Useful for view-only rollouts.
Hard cap. Inserts beyond this fail with 413.
Cap on distinct tables per app. Prevents schema sprawl.
When ON, every read is audit-logged. OFF by default because reads are high-volume; writes always audit.
When ON (default), sensitive: true columns encrypt at rest. When OFF, sensitive flag is ignored and rows go in plaintext.
What bundles can do
The SDK exposes window.platform.db.<table> with five methods. All requests go through a per-app HMAC-signed token so a bundle can only read its own app's schema.
// list with filters and pagination
const recent = await window.platform.db.notes.list({
where: { is_pinned: true },
orderBy: { column: "_created_at", direction: "desc" },
limit: 50,
});
// get one row by _id
const note = await window.platform.db.notes.get(id);
// insert (system columns stamped server-side)
const fresh = await window.platform.db.notes.insert({
title: "First note",
body: "Hello",
_visibility: "private", // optional override
});
// update (only owner or admin can write to a non-public row)
await window.platform.db.notes.update(id, { is_pinned: true });
// delete
await window.platform.db.notes.delete(id);The forigi.json schema spec
Every table in the manifest must declare its columns and (optionally) a default visibility. Maximum 32 user columns per table; 20 tables per app by default. System columns are added automatically.
{
"version": 1,
"tables": {
"notes": {
"default_visibility": "public",
"columns": {
"title": { "type": "string", "indexed": true, "max_length": 200 },
"body": { "type": "string", "max_length": 10000 },
"tag": { "type": "string", "indexed": true, "max_length": 50, "nullable": true },
"is_pinned": { "type": "boolean", "default": false },
"private_note": { "type": "string", "sensitive": true, "max_length": 5000 }
}
}
}
}Identifier rules: lowercase, snake_case, 1–41 chars, must start with a letter. Reserved names like users, session, audit are refused. System columns prefixed with _ are reserved.
What gets logged
- Schema changes — every approval and rejection, every column add, every table drop. Tamper-resistant: the audit table refuses UPDATEs and only allows DELETEs through a locked RPC.
- Writes — insert, update, delete. Always logged, regardless of tenant settings.
- Reads — only when the tenant has
app_database_audit_readsON. Default OFF because read volume can be high; flip ON for compliance regimes that require it.
Limits worth knowing
- No cross-app queries. An app's bundle can only access its own schema. Cross-app data sharing happens via SharePoint/OneDrive wirings, not the database.
- No raw SQL. The SDK is the only surface; it's intentionally narrow. CRUD operations only.
- No schema versioning UI yet. Destructive changes (renaming/removing columns) require dropping the table and re-approving. Coming in a later phase.
- No cross-tenant access. A bundle deployed in tenant A cannot read tenant B's data, even if both have the same app schema name.
For IT review
Need the full implementation detail (RLS policies, key derivation, audit-log trigger logic, threat model)? Email hello@forigi.com and we'll send the canonical doc directly.
Back to Getting started · Security policy