1# Nested app namespaces23One project often carries more than one app-shaped thing — a task tracker and a notes surface, say —4that should still share one Jazz store, one identity, and one durability story. `s.defineNestedApp`5groups tables into app-level namespaces over a single compiled schema: one schema hash, one6migration lineage, one query planner.78## Declare namespaces910```ts11// src/schema.ts12import { s } from "@nzip/lofi/schema";1314export const root = s.defineNestedApp({15 taskapp: {16 projects: s.table({ name: s.string() }),17 tasks: s.table({18 projectId: s.ref("projects"),19 text: s.string(),20 completed: s.boolean(),21 }),22 },23 notesapp: {24 notes: s.table({ title: s.string() }),25 },26});27```2829`root.taskapp.tasks` and `root.notesapp.notes` are ordinary typed table handles — queries, inserts,30and live stores treat them exactly like `s.defineApp` tables. Under the hood each nested table31flattens to a global name with the reserved `__` separator (`taskapp__tasks`), so namespace and32table names must not contain `__` or `.`.3334Handles are constructed once, inside `defineNestedApp`. The runtime keys table stores by handle35identity, so this is the only expressible construction shape and duplicate subscriptions cannot36arise — keep the declaration at module scope as with any lofi schema.3738Refs written against namespace-local names (`s.ref("projects")` above) resolve within the namespace.39To reference a table in another namespace, qualify it:4041```ts42attachment: s.ref("taskapp.tasks").optional(),43```4445## Permissions per namespace4647Each namespace gets its own permissions module. `s.defineNestedPermissions` exposes only that48namespace's tables, under their local names; the compiled output is keyed by the global (mangled)49table names, so per-namespace bundles merge collision-free:5051```ts52// src/permissions.ts53import { s } from "@nzip/lofi/schema";54import { root } from "./schema.ts";5556const taskPermissions = s.defineNestedPermissions(root.taskapp, ({ policy, session }) => {57 policy.tasks.allowInsert.always();58 policy.tasks.allowRead.where({ $createdBy: session.user_id });59 policy.projects.allowInsert.always();60 policy.projects.allowRead.always();61});6263const notePermissions = s.defineNestedPermissions(root.notesapp, ({ policy, session }) => {64 policy.notes.allowInsert.always();65 policy.notes.allowRead.where({ $createdBy: session.user_id });66});6768export default s.mergeNestedPermissions(taskPermissions, notePermissions);69```7071Use `s.defineNestedPermissions` — not `s.definePermissions` — on a namespace: the pinned72`definePermissions` keys its compiled rules by the object's property names, so calling it on a73namespace directly would compile unprefixed table names the deployed schema does not contain.7475## Register the app7677The nested root is the schema value of the one lofi app; the runtime consumes its flattened table78registry, so every nested table participates in boot readiness and local-to-managed row migration:7980```ts81// src/app.ts82import { defineLofiApp } from "@nzip/lofi";83import { root } from "./schema.ts";8485export const app = defineLofiApp({86 name: "my-suite",87 databaseName: "my-suite",88 schema: root,89 storage: "durable",90 sync: { adapter: "jazz" },91});92```9394## Migrations across namespaces9596Underneath it is still one schema, so the migration surface is unchanged. Author migrations over the97flattened definition (`s.flattenNestedSchema`), and moving a table between namespaces is an ordinary98`renameTableFrom`:99100```ts101const migration = s.defineMigration({102 from: s.defineSchema(s.flattenNestedSchema(v1)),103 to: s.defineSchema(s.flattenNestedSchema(v2)),104 renameTables: { notesapp__archive: s.renameTableFrom("taskapp__archive") },105 migrate: {},106});107```108109## Known constraint: g-set columns110111The conformance findings pin an alpha.53 engine issue where a `g-set` column destabilizes writes to112sibling tables in the same app (see113[the facade decision record](../decisions/schema-facade-alpha53.md)). The standing guidance is to114keep a g-set table in its own single-table app — and under a nested app every table is a runtime115sibling of every namespace, so that isolation cannot be satisfied inside one store. Keep g-set116columns out of nested apps until the upstream pin clears.