c413ab7ddwas reverted byc31dcdd66; the re-apply (3a7bc7f1c) only brought back the mana-research tests, not my sweep. Restored inaf4fd2776. Update the shipping-log row + the attribution note so future readers find the actual payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 KiB
Space-scoped data model (Modell β)
Started 2026-04-22.
Supersedes per-space-vs-user-global-tags.md — the earlier "defer" recommendation was made under "ship fast" assumptions; this plan assumes pre-live + unlimited resources, i.e. we build the clean architecture now without legacy residues.
Shipping log
| Phase | Purpose | Commit |
|---|---|---|
| 2a | Crypto-registry prep (enabled:false for globalTags/tagGroups/workbenchScenes/aiMissions) | 766ad2ea8 |
| 2b | Dexie v34: userTagPresets table + compound indexes on globalTags/tagGroups | 07e35d79f |
| 2d.1 | userTagPresets CRUD store + move into encryption registry | 35d9e023a |
| 2d.2 | kontextDoc per-Space (store + queries + AI-runner resolver) | 8a82f3c54 |
| 2d.3 | SpaceType-aware default agent bootstrap | a36e543e4 |
| 2d.4 | onActiveSpaceChanged subscriber + per-Space localStorage + scene filter | 3b85d7d3d ⚠️ |
| 2d.5a | applyPresetToSpace + copyTagsBetweenSpaces helpers | 596e5a742 |
| 2d.5b | SpaceCreateDialog tag-source picker | 81a426af2 |
| 2d.6 | Settings → Tag-Presets management UI | 0f8fbb381 |
| 2e | Encryption flip (enabled:true on 4 tables) | 09e6a8b9d |
| 2c | Creating-hook: stop stamping userId on data tables | e9b9544ea |
| 2e-followup | At-rest encrypt sweep (post-unlock, per-table sentinel) | af4fd2776 (was c413ab7dd, reverted + restored) |
| 2c-followup #1 | Dexie v35 hard userId-drop on data tables + drop dead indexes | f4c66241c |
| 2c-followup #2 | Dexie v36 strip spaceId/authorId/visibility from user-level tables | ce5d1f1a2 |
⚠️ 2d.4 + 2e-followup attribution note: Two commits absorbed
Space-scoped work under unrelated titles due to parallel-session
lint-staged rollback races (see feedback_git_workflow.md):
-
3b85d7d3d chore(bundle): add bundle-size audit …— contains the 2d.4 payload (active-space handler API + per-Space workbench-scenes localStorage + scene spaceId filter + runAgentsBootstrap on Space change). The bundle-audit files are legit too; the Space-switch work is the second half of the diff. -
c413ab7dd test(mana-research): fixture-based tests …— contained the 2e-followup payload (at-rest encrypt sweep). Later reverted byc31dcdd66and the re-apply (3a7bc7f1c) only restored the mana-research test files, dropping my sweep payload. Restored in its own commitaf4fd2776with the correct message + plan-doc shipping-log updated to point there.
Both commits' code is correct and typechecks cleanly. Grep for
runAtRestEncryptSweep / onActiveSpaceChanged to find the actual
payload.
Decision
Everything that the user creates — tags, tag-groups, workbench scenes, AI agents, AI missions, kontextDoc — lives in a Space. Only identity, authentication, session, profile, master-key material, and per-device UI preferences stay at the user level.
Two crisp levels:
| Level | What lives here | Examples |
|---|---|---|
| User | Identity · auth · MK key · cross-device profile · preferences that follow the user · tag presets (user-level templates for new Spaces) | user, session, authKeys, profile, userSettings, userTagPresets |
| Space | Every data record the user creates or interacts with | Tasks · events · notes · contacts · dreams · memoros · tags · tag-groups · scenes · agents · missions · kontextDoc · workbench layouts — everything |
| Device | Ephemeral UI state that's explicitly per-device | Active scene per Space · theme toggle · panel sizes · last-used module — all via localStorage |
"Device" isn't a data model — it's localStorage conventions. Listed for completeness so nothing falls through the cracks.
Why β, not γ (recursive Context)
- β has two clear data levels — Space (tenant) vs. Scene-as-filter (view inside that Space). Users always know where they are.
- γ (one recursive
Contextprimitive with arbitrary nesting) is more flexible but mentally heavier — "am I in a Space or a sub-Space?" is a real question users would have to answer. - β is a natural stepping stone if we ever want γ: a γ-Context is just a β-Space with children.
No legacy residues
Explicit anti-patterns this plan rejects (so we don't drift back into them):
- No
userIdaudit column on data records. Attribution lives in the Actor system (__lastActor,__fieldActors— added during the AI-Workbench rollout) on every record. A separateuserIdwould be redundant. Members of a Space see each others' Actor attributions; that's the correct model for shared-Space collaboration. - No "user-global tag" hybrid. Tags always belong to a Space. Users who want a taxonomy to propagate across their Spaces create a tag preset at user level (see §5) and seed new Spaces from it.
- No single-ID
activeSceneIdin localStorage. The active scene is per-Space-per-device; the key ismana:workbench:activeSceneId:${spaceId}. - No plaintext-tag defer. User-typed tag names (
Therapie,Finanzen-privat) can leak personal categorization. Encrypt during the migration, not as a follow-up. - No "one default agent everywhere" name collision. Default-agent bootstrap uses a SpaceType-aware name (see §4).
- No user-level
kontextDocsingleton. The AI planner's auto- injected context is per-Space (a Family-Space doesn't want to see the user's Brand-Space bio).
Scope
In scope (become Space-scoped)
Confirmed by Phase 1 audit (see appendix at bottom of this doc):
globalTags+tagGroups— addspaceId, dropuserId, encryptname+icon.workbenchScenes— addspaceId. Layout +scopeTagIdsstay. Encrypttitle.aiAgents— addspaceId. Bootstrap runs per Space.aiMissions— addspaceId. Follows agents.kontextDoc— reshape from user-level singleton to per-Space (one per Space, keyed[spaceId, type: 'kontextDoc']).agentKontextDocs(v22, per-agent context docs — added by audit) — addspaceIdvia agent FK lookup. Ordered: migrateaiAgentsfirst, then backfillagentKontextDocs.spaceIdfrom the parent.
Also in scope: drop userId across all already-migrated tables
The Phase 1 audit found that all 46 previously-migrated
space-scoped tables still carry userId (redundant with Actor
attribution). To satisfy the "no table has both userId and
spaceId" invariant, Phase 2 drops userId from every data-record
table, not just the 7 newly-migrated ones. This is a ~53-table
sweep, mechanically identical per table, so the work scales cheaply.
Stays user-level (identity / preferences)
user,session,authKeys, MK key wrappingprofile(bio, avatar, locale, timezone)userSettings— user-wide prefs (theme default, locale)userTagPresets— new table (see §5)- Access-tier claim on the JWT
Ephemeral, per-device (localStorage only)
- Active scene per Space:
mana:workbench:activeSceneId:${spaceId} - Active space hint:
mana:scope.activeSpaceId(already exists) - Theme toggle, panel sizes, last-used module, debug flags
Already correctly Space-scoped
The ~46 module tables migrated in the Spaces-Foundation sprint. Nothing to do.
Junction tables — must be audited in Phase 1
All 19 tag junction tables (taskLabels, eventTags, contactTags,
…) are FK-only and inherit spaceId from their parent record. Phase 1
confirms that every junction row's parent actually carries
spaceId — any junction referencing a still-user-scoped table is a
migration bug. Full list generated via codebase audit, not assumed.
Target schemas
globalTags
interface LocalTag {
id: string;
spaceId: string; // NEW — required, indexed
name: string; // encrypted (see Phase 2)
color: string;
icon?: string; // encrypted
groupId?: string;
sortOrder: number;
createdAt: string;
updatedAt: string;
deletedAt?: string;
__fieldTimestamps?: Record<string, number>;
__lastActor?: Actor; // from AI-Workbench rollout
__fieldActors?: Record<string, Actor>;
// NO userId
}
Dexie indexes: id, spaceId, [spaceId+name], [spaceId+sortOrder].
Crypto registry (apps/mana/apps/web/src/lib/data/crypto/registry.ts):
globalTags: {
encrypted: ['name', 'icon'],
plaintext: ['id', 'spaceId', 'color', 'groupId', 'sortOrder',
'createdAt', 'updatedAt', 'deletedAt',
'__fieldTimestamps', '__lastActor', '__fieldActors'],
}
tagGroups
Same treatment. spaceId required, name encrypted.
workbenchScenes
Already has title, layout, scopeTagIds?. Add spaceId. Encrypt
title (it's user-authored).
aiAgents, aiMissions
spaceId required. Names/system-prompt/memory/mission-objective are
already encrypted (per AI-Workbench rollout). Nothing changes on the
crypto side except adding a spaceId plaintext column to the
registry entries.
kontextDoc
Today a user-level singleton. New shape:
interface LocalKontextDoc {
id: string; // uuid, one per Space
spaceId: string; // required, unique in-space
type: 'kontextDoc'; // discriminator, for the [spaceId+type] unique index
content: string; // encrypted (already is)
updatedAt: string;
__lastActor?: Actor;
__fieldActors?: Record<string, Actor>;
}
Dexie indexes: id, spaceId, [spaceId+type].
The AI runner's auto-injection logic switches from
getUserKontextDoc() to getKontextDocForActiveSpace().
userTagPresets (NEW — user-level)
interface LocalUserTagPreset {
id: string;
userId: string; // explicit — this table is user-scoped
name: string; // "Mein Standard-Set", "Brand-Setup", …
isDefault: boolean; // at most one default per user
tags: Array<{
name: string;
color: string;
icon?: string;
groupName?: string;
}>;
createdAt: string;
updatedAt: string;
}
This is a first-class template for seeding new Spaces, not a live link. Values are frozen snapshots of tag definitions. When creating a new Space, the user picks a preset (or none); tags from the preset are one-shot-copied into the new Space with fresh UUIDs.
Lives outside any Space in the Dexie DB. Synced cross-device per-
user. Encrypted same as globalTags (tag names inside are sensitive).
Phases
Phase 1 — Audit + schema finalisation (0.5 day)
- Enumerate every Dexie
store(...)definition inapps/mana/apps/web/src/lib/data/database.ts. Cross-check againstpackages/shared-types. - For every table, decide:
- Space-scoped (majority — migrate)
- User-level (only identity / profile / preferences /
userTagPresets) - Junction (inherits from parent — verify parent has
spaceId)
- Verify every junction row's parent is Space-scoped. Any junction still pointing at a user-global table is a bug to flag.
- Verify Actor columns (
__lastActor,__fieldActors) are on every migration target — so we can confidently dropuserId. - Deliverable: concrete table list in an appendix to this doc, committed before Phase 2 runs. Any surprises get called out.
Phase 2 — Dexie migration (1.5 days)
- Bump Dexie version. Write migration function that for each target
table:
- Adds
spaceId = user.personalSpaceId(signup hook guarantees it exists). - Drops
userId— attribution is the Actor system's job. (__lastActoris already populated for records created after the AI-Workbench rollout; pre-rollout rows get__lastActor = { kind: 'user', principalId: userId, displayName: user.name }during the migration — a one-time backfill, thenuserIdis deleted). - For
globalTags/tagGroups/workbenchScenes: encryptname/title/iconduring migration. Register in crypto registry. Dev-mode drift check catches missed fields. - For
kontextDoc: reshape into{ id, spaceId, type: 'kontextDoc', content, ... }. Each user has exactly one pre-migration kontextDoc — it becomes the Personal-Space's kontextDoc. Other Spaces get no kontextDoc until the user writes one.
- Adds
- Register new tables with
scopedForModule():scopedForModule('tags', 'globalTags'),scopedForModule('workbench', 'workbenchScenes'),scopedForModule('ai', 'aiAgents'),scopedForModule('ai', 'aiMissions'),scopedForModule('ai', 'kontextDoc'). - Create the new
userTagPresetstable — user-level, encryptedname+ inline tag names. database.tscreating hook: stampspaceIdfromgetActiveSpaceId(). ThrowScopeNotReadyErrorif unset — guards against races.- Remove
userIdstamping for Space-scoped tables (the creating hook stops stamping it since the column is gone). - Unit tests (fake-indexeddb): round-trip a tag through encrypt/write/read in two different Spaces, verify isolation. Round-trip a preset apply, verify tags land with correct spaceId + fresh ids.
Phase 3 — Store APIs + runner integration (1 day)
packages/shared-stores/src/tags-local.svelte.ts:useAllTags()— unchanged surface; implicit Space filter fromscopedForModule(). Returned tags havenamealready decrypted (crypto pipeline).tagMutations.createTag(input)— nospaceIdargument; hook stamps it. Refuses explicit cross-Space writes.
userTagPresetsstore:useTagPresets(),createPreset(),applyPresetToSpace(presetId, spaceId)— the apply flow one-shot-copies preset entries into the target Space as freshglobalTagsrows.workbenchScenesStore:useScenes()→ Space-filtered live query.- Per-Space default scene bootstrap (see Phase 4).
- AI agents store:
bootstrapDefaultAgent(spaceId, spaceType)— idempotent, keyed[spaceId, kind: 'default']. Name derived fromspaceType:personal→ "Mana"family→ "Familien-Helfer"team→ "Team-Assistent"brand→ "Brand-Assistent"club→ "Verein-Helfer"practice→ "Praxis-Assistent"
- Users rename freely after bootstrap.
- AI runner (
src/lib/data/ai/missions/runner.ts):- Replace
getUserKontextDoc()withgetKontextDocForActiveSpace(). If none exists for the active Space, skip injection (not an error — users opt in by writing one).
- Replace
Phase 4 — Space-switch behavior (0.5 day)
apps/mana/apps/web/src/lib/data/scope/active-space.svelte.ts:
After loadActiveSpace() completes and sets active = space, fire
onActiveSpaceChanged(space):
- Bootstrap singletons if missing (idempotent):
- Default agent for this Space's type (see §3).
- Default scene "Übersicht" with empty layout.
- Restore per-device active scene for this Space from
localStorage.getItem('mana:workbench:activeSceneId:' + space.id). If null or stale, fall back to the Space's default scene. - No data reset — all reactive queries already pivot via
scopedForModule()on the newactiveSpaceId. Module stores don't need custom reset logic. - Per-device UI state is preserved — theme, panel sizes, last-used module id don't change.
workbenchScenesStore.setActiveScene(sceneId) writes the per-Space
localStorage key, not the old single-key one.
Phase 5 — Space creation + preset application (0.5 day)
apps/mana/apps/web/src/lib/components/layout/SpaceCreateDialog.svelte:
Add a "Tag-Set" section to the create dialog:
- Dropdown of the user's
userTagPresets(if any), plus built-in options: "Leer" (default for Shared-Spaces) and "Aus Personal kopieren" (default for Solo-Spaces — a convenience shortcut that copies the Personal-Space's current tags as a one-shot). - On submit: create Space → bootstrap default agent + default scene → if a preset / copy-from-Personal was chosen, one-shot-copy tags.
Admin-style entry in the user's Settings → "Tag-Presets" to
create/edit presets (CRUD for userTagPresets). Any existing Space's
tag set can be "exported as preset" from its TagManager UI.
Phase 6 — Backend (mana-sync + Postgres + RLS) (1 day)
- PostgreSQL migration: add
space_id text not nullon sync tables forglobalTags,tag_groups,workbench_scenes,ai_agents,ai_missions,kontext_docs. Dropuser_idfrom the first four (keep only on the newuser_tag_presetstable, which remains user-scoped).- Two-step Postgres backfill: add nullable
space_id, backfill via join (update … set space_id = personal_space_of(user_id)), thenset not null, then dropuser_id.
- Two-step Postgres backfill: add nullable
- RLS policies: membership-based for every Space-scoped table (matches existing 46-table pattern).
user_tag_presets: RLS =user_id = current_user_id().- mana-sync config: register new Space-scoped tables in the
collection→space-table mapping; register
user_tag_presetsin the user-scoped mapping.
Phase 7 — Cleanup + docs (0.5 day)
- Delete the
// TODO: auditencryption comment onglobalTags— resolved. - Update
apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md: canonical rule is now "data records carryspaceId, notuserId; attribution lives in Actor columns". DocumentuserTagPresetsas the only user-scoped data table. - Add a section to
apps/mana/CLAUDE.md:Adding a new top-level table? It gets
spaceId, notuserId. User-level is reserved for identity, auth, profile, and explicit user-level templates (likeuserTagPresets). Attribution on every record is via__lastActor/__fieldActors. - Update memory files:
feedback_cards_over_subroutes.md— note that tags, scenes, agents, missions, kontextDoc are all Space-scoped.- Add
project_space_scoped_datamodel.mdwith commit refs after shipping.
- Regenerate
validate:alloutputs — the three validation scripts (turbo recursion, pgSchema, crypto registry) should all stay green since Phase 2 wired the new tables in from the start.
Phase 8 — Delete the deprecated plan (5 min)
per-space-vs-user-global-tags.md gets deleted in the same PR as
Phase 7. Git history preserves the deferred-decision reasoning for
future readers who dig; the current tree shouldn't confuse anyone.
Edge cases & decisions (baked in)
Member of another user's Shared-Space at migration time? Existing user-global tags/scenes/agents go into your Personal-Space. Shared Spaces start empty from your side; other members' content is visible via RLS once you join.
Signup hook failed — no Personal-Space? Phase 2 migration refuses to run. Repair that user's state first, then retry.
kontextDoc in non-Personal Spaces? Absent by default. User
writes one explicitly if the planner should inject Space-specific
context. UI exposes "Space-Kontext bearbeiten" in each Space's
Settings.
Default agent name collision across user's Spaces? Names are SpaceType-aware, so three Spaces give three different defaults. Users can rename freely — the bootstrap only runs once per Space.
What if a user deletes their only userTagPreset? That's fine —
new-Space dialog falls back to "Leer" / "Aus Personal kopieren".
Presets are a convenience, not a dependency.
Cross-device preset drift? userTagPresets syncs via mana-sync
with user-level RLS. Same model as profile.
Success criteria
Before merging:
- No table in the Dexie DB has both
userIdandspaceIdcolumns (the canonical rule is one-or-the-other, based on the Space-scoped vs. user-level decision). validate:allgreen: turbo recursion · pgSchema · crypto registry · theme tokens · CSS utilities.- Switching active Space in the UI flips: tag list · scene list · agent list · mission list · kontextDoc — all at once, no stale state in any module.
- Creating a new Shared-Space starts with an empty tag list. Creating a new Solo-Space offers the preset picker with user-defined options + the "Aus Personal kopieren" shortcut.
- Active scene persists per-Space-per-device: switch Space A → scene X, switch to Space B → scene Y, switch back to A → scene X restored.
- AI runner in Family-Space injects the Family-Space
kontextDoc, not Personal's. - Default agents bootstrap with SpaceType-aware names — three Spaces of different types show three distinct default agents.
- Tag names are encrypted at rest (verified via direct Dexie
inspection showing ciphertext for
name/icon). - Smoke: in Personal-Space, create tag "Urgent". Switch to a new
Shared-Space.
useAllTags()returns empty. Switch back. "Urgent" visible. Create preset from Personal. Create new Brand-Space with that preset. "Urgent" appears there (with a differenttag.id, confirming it's a copy not a reference).
Timeline
~4–5 focused days across all phases (up from 3–4 in the previous
draft; the extra day covers encryption wiring, userTagPresets CRUD,
and the userId → Actor attribution cleanup).
Related
spaces-foundation.md— the Spaces primitive this plan extends.workbench-cards-migration.md— the cards-vs-routes policy from the same strategic session.scene-scope-empty-state.md— UX for the scope-filter empty state; integrates naturally once scenes are per-Space.- Deprecated by this doc:
per-space-vs-user-global-tags.md— delete in Phase 8.
Backend coherence (audited 2026-04-22 post-migration)
mana-sync uses a single-table event-sourcing model: every change
from every client collection lands in one Postgres sync_changes
table with a table_name discriminator. There are no per-collection
tables on the server side — no tasks table, no tags table, no
agents table. The 7 newly-migrated client tables therefore do NOT
need their own server-side DDL.
What matters server-side: the sync_changes table has a space_id TEXT column with a partial index and a sync_changes_space_member_read
RLS policy that filters by Space membership. The handler extracts
spaceId from the client payload (top-level, data.spaceId, or
fields.spaceId.value) and passes it through RecordChange() into
the RLS-wrapped insert.
All seven of our newly-scoped client tables were already in
SYNC_APP_MAP before this migration; their writes just started
carrying spaceId more consistently. No backend changes required.
See services/mana-sync/internal/store/postgres.go (lines 45–135 for
schema + RLS) and services/mana-sync/internal/sync/handler.go
(extractSpaceID helper, ~line 58) for the pattern.
Appendix — Phase 1 audit results (2026-04-22)
Source: full audit of apps/mana/apps/web/src/lib/data/database.ts,
crypto/registry.ts, crypto/plaintext-allowlist.ts. Conclusion:
no surprises that block Phase 2, with the two scope adjustments
already folded into the In-scope section above.
To-migrate (7 tables)
| Table | Current columns | Needs Actor? | Notes |
|---|---|---|---|
globalTags |
id, name, groupId, color, icon, sortOrder, userId, NO spaceId | ✗ — stamping needed | Add spaceId. Drop userId. Encrypt name + icon. |
tagGroups |
id, name, color, userId, NO spaceId | ✗ — stamping needed | Add spaceId. Drop userId. Encrypt name. |
workbenchScenes |
id, title, layout, scopeTagIds?, order, userId, NO spaceId | ✗ — stamping needed | Add spaceId. Drop userId. Encrypt title. |
aiAgents |
id, name, state, systemPrompt🔒, memory🔒, userId, NO spaceId | ✗ — stamping needed | Add spaceId. Drop userId. Name stays plaintext (display key). |
aiMissions |
id, state, createdAt, nextRunAt, userId, NO spaceId | ✗ — stamping needed | Add spaceId. Drop userId. |
kontextDoc |
id (singleton), content🔒 | ✗ — stamping needed | Reshape: [spaceId, type: 'kontextDoc'] PK. Pre-migration singleton becomes Personal-Space's kontextDoc. Other Spaces start without one. |
agentKontextDocs |
id, agentId, content🔒, NO spaceId | ✗ — stamping needed | Added by audit. Backfill spaceId via parent-agent lookup. Not in original plan. |
Legend: 🔒 = already encrypted in crypto registry.
Already space-scoped (46 tables) — userId cleanup sweep
All 46 tables migrated during the Spaces-Foundation sprint carry
both spaceId and userId. Phase 2 drops the redundant
userId — the migration helper runs over every table in this set
mechanically.
Sample (full list derived at implementation time from the Dexie
schema registry): tasks, events, notes, contacts,
conversations, documents, dreams, memos, meditations,
images, files, skills, plants, songs, ccLocations,
places, presiDecks, cardDecks, articles (v33), …
The creating-hook in database.ts is updated to stop stamping
userId on these tables (Phase 2). Attribution reads from
__lastActor everywhere.
User-level (10 existing + 1 new) — NO change
| Table | Purpose |
|---|---|
userSettings |
user-wide UI defaults (theme, locale) |
userContext |
profile hub (about, interests, routine, goals, social) |
newsPreferences |
feed subscriptions + learned weights |
meditateSettings |
meditation prefs |
sleepSettings |
sleep-tracking prefs |
moodSettings |
mood-logging prefs |
timeSettings |
time-tracking prefs |
invoiceSettings |
sender profile (legal address, IBAN) |
broadcastSettings |
newsletter sender defaults |
wetterSettings |
weather prefs |
userTagPresets (new in Phase 2) |
user-level templates for Space-seeding |
userContext is not the same as kontextDoc — the former is
the user's profile hub (identity-level bio/interests), the latter
is the AI-planner-injected context (moves per-Space). They serve
different roles. No collision.
Junction tables (19) — all parents space-scoped ✓
No dangling references. Every junction inherits spaceId from its
parent once parents are migrated.
taskLabels · eventTags · contactTags · conversationTags ·
documentTags · imageTags · fileTags · photoMediaTags ·
memoTags · mealTags · plantTags · songTags · skillTags ·
ccLocationTags · placeTags · presiDeckTags · deckTags ·
articleTags · noteTags · timeBlockTags
(taskLabels points at globalTags — after globalTags gets
spaceId, the junction's [taskId+labelId] pair is implicitly
within-Space via the parent task's spaceId.)
Internal / infrastructure (10) — special handling
| Table | Handling |
|---|---|
_pendingChanges |
Add spaceId column from pending-change payload so sync routes know the partition. |
_syncMeta |
Infra-level; no spaceId. |
_activity |
Deprecated, superseded by _events. Leave read-only. |
_events |
Local-only domain event store. Event metadata already carries appId + recordId; no spaceId needed at event layer (materialized downstream). |
_eventsTombstones |
Sync tombstones; no spaceId (appId+collection+recordId sufficient). |
_memory, _nudgeOutcomes |
Companion-brain local memory; never synced. No spaceId. |
_byokKeys |
BYOK master-key storage. Device-local, encrypted. User-level by nature. |
_aiDebugLog |
Per-iteration LLM debug capture. Capped. Never synced (leaks decrypted content). No spaceId. |
_streakState |
Companion engagement tracker. Local-only. |
_serverIterationExecutions |
Idempotency marker for server-source AI iterations. Local-only. |
Scope summary
- To-migrate: 7 tables (add
spaceId+ dropuserId+ encrypt as noted) - userId sweep: 46 already-space-scoped tables (drop
userIdonly) - User-level: 10 existing + 1 new (
userTagPresets) — no change to these - Junctions: 19 — all parents space-scoped, no action needed
- Internal/infra: 10 — handled per table-type, mostly no action
No blockers. Phase 2 can proceed.