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.

Architecture

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 (public or private). Stamped server-side; bundles cannot forge them.
Security

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.
Encryption

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
Controls

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.

app_databases_allowed

Master switch. When OFF, the SDK's window.platform.db calls return APP_DATABASES_DISABLED. Default OFF.

app_database_inference_via_llm

When ON, schema inference can call Anthropic with the bundle's source code (never data rows). Default OFF; manifests required.

app_database_user_writes_allowed

When OFF, only app owners and tenant admins can write. Useful for view-only rollouts.

app_database_max_rows_per_table

Hard cap. Inserts beyond this fail with 413.

app_database_max_tables_per_app

Cap on distinct tables per app. Prevents schema sprawl.

app_database_audit_reads

When ON, every read is audit-logged. OFF by default because reads are high-volume; writes always audit.

app_database_sensitive_column_encryption

When ON (default), sensitive: true columns encrypt at rest. When OFF, sensitive flag is ignored and rows go in plaintext.

SDK

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);
Manifest

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.

Audit

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_reads ON. Default OFF because read volume can be high; flip ON for compliance regimes that require it.
What it doesn't do

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