Skip to content

Commit 274cdc4

Browse files
authored
Merge pull request #1520 from EpicenterHQ/opencode/curious-garden
refactor(api): include type segment in DO names
2 parents 9930e31 + c04f865 commit 274cdc4

3 files changed

Lines changed: 109 additions & 5 deletions

File tree

apps/api/src/app.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ app.post(
258258
// ---------------------------------------------------------------------------
259259

260260
/**
261-
* DO name namespacing: `user:{userId}:{workspace|document}`
261+
* DO name namespacing: `user:{userId}:{type}:{name}`
262262
*
263263
* We use user-scoped DO names (Google Docs model) rather than org-scoped names
264264
* (Vercel/Supabase model). Each user gets their own DO instance per workspace.
@@ -287,13 +287,13 @@ app.post(
287287

288288
/** Get a WorkspaceRoom DO stub for the authenticated user's workspace. */
289289
function getWorkspaceStub(c: Context<Env>) {
290-
const doName = `user:${c.var.user.id}:${c.req.param('workspace')}`;
290+
const doName = `user:${c.var.user.id}:workspace:${c.req.param('workspace')}`;
291291
return c.env.WORKSPACE_ROOM.get(c.env.WORKSPACE_ROOM.idFromName(doName));
292292
}
293293

294294
/** Get a DocumentRoom DO stub for the authenticated user's document. */
295295
function getDocumentStub(c: Context<Env>) {
296-
const doName = `user:${c.var.user.id}:${c.req.param('document')}`;
296+
const doName = `user:${c.var.user.id}:document:${c.req.param('document')}`;
297297
return c.env.DOCUMENT_ROOM.get(c.env.DOCUMENT_ROOM.idFromName(doName));
298298
}
299299

apps/api/src/base-sync-room.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ type SyncRoomConfig = {
8787
* before calling RPC methods or forwarding fetch. The DO itself does not
8888
* re-validate — it trusts the Worker boundary.
8989
*
90-
* DO names are user-scoped: the Worker prefixes `user:{userId}:` to the
91-
* client-provided workspace or document name before calling `idFromName()`.
90+
* DO names are user-scoped: the Worker constructs
91+
* `user:{userId}:{type}:{name}` before calling `idFromName()`, where
92+
* `{type}` is `workspace` or `document`.
9293
* This ensures each user's data is isolated in separate DO instances, even
9394
* if multiple users create workspaces with the same name (e.g., "tab-manager").
9495
*
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# DO Naming Convention: Include Type Segment
2+
3+
**Status:** Pending
4+
**Scope:** 2 files, ~10 line changes
5+
**Worktree:** Separate branch off `main` (not this branch)
6+
7+
## Problem
8+
9+
DO names are `user:{userId}:{resourceName}` for both workspace and document routes. Since `WORKSPACE_ROOM` and `DOCUMENT_ROOM` are separate CF namespaces, `idFromName()` produces different DO instances—but the name strings are identical. This causes a bug in the `durable_object_instance` tracking table (see sibling spec) where a UNIQUE constraint on `doName` collides when a user has a workspace and document with the same `resourceName`.
10+
11+
More fundamentally, the current naming scheme doesn't encode **what kind of DO** it is. Given the string `user:abc:notes`, you can't tell if it's a workspace or a document without additional context.
12+
13+
## Decision
14+
15+
**All DO names include the type segment: `user:{userId}:{doType}:{resourceName}`.**
16+
17+
| Route | Old DO Name | New DO Name |
18+
|---|---|---|
19+
| `GET /workspaces/epicenter.tab-manager` | `user:abc:epicenter.tab-manager` | `user:abc:workspace:epicenter.tab-manager` |
20+
| `GET /documents/my-note` | `user:abc:my-note` | `user:abc:document:my-note` |
21+
22+
### Why this matters
23+
24+
1. **Self-documenting**—the DO name tells you exactly what it is
25+
2. **Globally unique across namespaces**—no collisions in tracking tables
26+
3. **Reverse-lookup friendly**—given a DO name from logs/analytics, you can parse out type + resource
27+
4. **Future-proof**—if a third DO type is added, the pattern scales
28+
29+
### Migration strategy
30+
31+
**Clean break.** Same approach as the `tab-manager``epicenter.tab-manager` rename. Local-first clients hold the full Y.Doc and will re-sync to the new (empty) DO on next connection. Old DOs sit idle and can be cleaned up later.
32+
33+
**Document snapshots will be lost** for `DocumentRoom` DOs (stored in the old DO's SQLite). This is acceptable during early development—no production users have critical snapshot history yet.
34+
35+
## Implementation Plan
36+
37+
### Task 1: Update stub functions in `app.ts`
38+
39+
- [ ] Change `getWorkspaceStub` DO name: `user:${userId}:${workspace}``user:${userId}:workspace:${workspace}`
40+
- [ ] Change `getDocumentStub` DO name: `user:${userId}:${document}``user:${userId}:document:${document}`
41+
42+
**`getWorkspaceStub` (app.ts:294–297):**
43+
44+
```typescript
45+
function getWorkspaceStub(c: Context<Env>) {
46+
const doName = `user:${c.var.user.id}:workspace:${c.req.param('workspace')}`;
47+
return c.env.WORKSPACE_ROOM.get(c.env.WORKSPACE_ROOM.idFromName(doName));
48+
}
49+
```
50+
51+
**`getDocumentStub` (app.ts:300–303):**
52+
53+
```typescript
54+
function getDocumentStub(c: Context<Env>) {
55+
const doName = `user:${c.var.user.id}:document:${c.req.param('document')}`;
56+
return c.env.DOCUMENT_ROOM.get(c.env.DOCUMENT_ROOM.idFromName(doName));
57+
}
58+
```
59+
60+
### Task 2: Update JSDoc comments
61+
62+
- [ ] Update the DO name namespacing JSDoc block above `getWorkspaceStub` (app.ts:265–291)
63+
- [ ] Update JSDoc in `base-sync-room.ts` (line 90–93) that references the naming scheme
64+
65+
**JSDoc update in app.ts (first line of the block, line 266):**
66+
67+
```
68+
- * DO name namespacing: `user:{userId}:{workspace|document}`
69+
+ * DO name namespacing: `user:{userId}:{type}:{name}`
70+
```
71+
72+
**JSDoc update in base-sync-room.ts (lines 90–93):**
73+
74+
```
75+
- * DO names are user-scoped: the Worker prefixes `user:{userId}:` to the
76+
- * client-provided workspace or document name before calling `idFromName()`.
77+
+ * DO names are user-scoped: the Worker constructs
78+
+ * `user:{userId}:{type}:{name}` before calling `idFromName()`, where
79+
+ * `{type}` is `workspace` or `document`.
80+
* This ensures each user's data is isolated in separate DO instances, even
81+
* if multiple users create workspaces with the same name (e.g., "epicenter.tab-manager").
82+
```
83+
84+
### Verification
85+
86+
- [ ] `bun run typecheck` passes from `apps/api/`
87+
- [ ] Grep for old pattern `user:\${c.var.user.id}:\${c.req.param` returns 0 results in `apps/api/src/`
88+
- [ ] Grep for new pattern `user:\${c.var.user.id}:workspace:` and `user:\${c.var.user.id}:document:` returns expected results
89+
90+
## Files Changed
91+
92+
- `apps/api/src/app.ts``getWorkspaceStub`, `getDocumentStub`, JSDoc
93+
- `apps/api/src/base-sync-room.ts` — JSDoc comment only
94+
95+
## Commit
96+
97+
```
98+
refactor(api): include type segment in DO names
99+
100+
Change DO naming from `user:{userId}:{name}` to
101+
`user:{userId}:{type}:{name}` so names are globally unique across
102+
DO namespaces. Clean break—clients re-sync to new DOs.
103+
```

0 commit comments

Comments
 (0)