engrm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.mcp.json +9 -0
  2. package/AUTH-DESIGN.md +436 -0
  3. package/BRIEF.md +197 -0
  4. package/CLAUDE.md +44 -0
  5. package/COMPETITIVE.md +174 -0
  6. package/CONTEXT-OPTIMIZATION.md +305 -0
  7. package/INFRASTRUCTURE.md +252 -0
  8. package/LICENSE +105 -0
  9. package/MARKET.md +230 -0
  10. package/PLAN.md +278 -0
  11. package/README.md +121 -0
  12. package/SENTINEL.md +293 -0
  13. package/SERVER-API-PLAN.md +553 -0
  14. package/SPEC.md +843 -0
  15. package/SWOT.md +148 -0
  16. package/SYNC-ARCHITECTURE.md +294 -0
  17. package/VIBE-CODER-STRATEGY.md +250 -0
  18. package/bun.lock +375 -0
  19. package/hooks/post-tool-use.ts +144 -0
  20. package/hooks/session-start.ts +64 -0
  21. package/hooks/stop.ts +131 -0
  22. package/mem-page.html +1305 -0
  23. package/package.json +30 -0
  24. package/src/capture/dedup.test.ts +103 -0
  25. package/src/capture/dedup.ts +76 -0
  26. package/src/capture/extractor.test.ts +245 -0
  27. package/src/capture/extractor.ts +330 -0
  28. package/src/capture/quality.test.ts +168 -0
  29. package/src/capture/quality.ts +104 -0
  30. package/src/capture/retrospective.test.ts +115 -0
  31. package/src/capture/retrospective.ts +121 -0
  32. package/src/capture/scanner.test.ts +131 -0
  33. package/src/capture/scanner.ts +100 -0
  34. package/src/capture/scrubber.test.ts +144 -0
  35. package/src/capture/scrubber.ts +181 -0
  36. package/src/cli.ts +517 -0
  37. package/src/config.ts +238 -0
  38. package/src/context/inject.test.ts +940 -0
  39. package/src/context/inject.ts +382 -0
  40. package/src/embeddings/backfill.ts +50 -0
  41. package/src/embeddings/embedder.test.ts +76 -0
  42. package/src/embeddings/embedder.ts +139 -0
  43. package/src/lifecycle/aging.test.ts +103 -0
  44. package/src/lifecycle/aging.ts +36 -0
  45. package/src/lifecycle/compaction.test.ts +264 -0
  46. package/src/lifecycle/compaction.ts +190 -0
  47. package/src/lifecycle/purge.test.ts +100 -0
  48. package/src/lifecycle/purge.ts +37 -0
  49. package/src/lifecycle/scheduler.test.ts +120 -0
  50. package/src/lifecycle/scheduler.ts +101 -0
  51. package/src/provisioning/browser-auth.ts +172 -0
  52. package/src/provisioning/provision.test.ts +198 -0
  53. package/src/provisioning/provision.ts +94 -0
  54. package/src/register.test.ts +167 -0
  55. package/src/register.ts +178 -0
  56. package/src/server.ts +436 -0
  57. package/src/storage/migrations.test.ts +244 -0
  58. package/src/storage/migrations.ts +261 -0
  59. package/src/storage/outbox.test.ts +229 -0
  60. package/src/storage/outbox.ts +131 -0
  61. package/src/storage/projects.test.ts +137 -0
  62. package/src/storage/projects.ts +184 -0
  63. package/src/storage/sqlite.test.ts +798 -0
  64. package/src/storage/sqlite.ts +934 -0
  65. package/src/storage/vec.test.ts +198 -0
  66. package/src/sync/auth.test.ts +76 -0
  67. package/src/sync/auth.ts +68 -0
  68. package/src/sync/client.ts +183 -0
  69. package/src/sync/engine.test.ts +94 -0
  70. package/src/sync/engine.ts +127 -0
  71. package/src/sync/pull.test.ts +279 -0
  72. package/src/sync/pull.ts +170 -0
  73. package/src/sync/push.test.ts +117 -0
  74. package/src/sync/push.ts +230 -0
  75. package/src/tools/get.ts +34 -0
  76. package/src/tools/pin.ts +47 -0
  77. package/src/tools/save.test.ts +301 -0
  78. package/src/tools/save.ts +231 -0
  79. package/src/tools/search.test.ts +69 -0
  80. package/src/tools/search.ts +181 -0
  81. package/src/tools/timeline.ts +64 -0
  82. package/tsconfig.json +22 -0
@@ -0,0 +1,553 @@
1
+ # Server-Side API Plan — Engrm
2
+
3
+ **Status**: Approved (Devstral review: 2026-03-11)
4
+ **Target**: Candengo Vector server (`/Volumes/Data/devs/candengo-vector/`)
5
+ **Stack**: FastAPI + Python, Qdrant (embedded), SQLite, Stripe
6
+
7
+ ---
8
+
9
+ ## 1. Protocol Fixes (Pre-Blockers)
10
+
11
+ ### 1.1 Bearer Auth Support
12
+ Add `Authorization: Bearer <cvk_key>` support to `app/auth.py` `get_auth_context()`.
13
+ - Extract token from Bearer header alongside existing `X-Api-Key`
14
+ - Validate `cvk_` prefix before DB lookup
15
+ - Keep both headers supported — existing clients use `X-Api-Key`
16
+
17
+ ### 1.2 Client Search Params
18
+ Fix client-side (`engrm/src/sync/client.ts`) to use `query`/`limit` instead of `q`/`top_k`.
19
+ Server schema is already deployed — don't add aliases.
20
+
21
+ ### 1.3 Bulk Delete
22
+ **`POST /v1/documents/delete-batch`**
23
+ - Body: `{ source_ids: string[], namespace?: string }`
24
+ - Response: `{ deleted: int }`
25
+ - Iterates source_ids, calls `vector_store.delete_by_source()` for each
26
+ - Auth: requires `ingest` scope
27
+
28
+ ---
29
+
30
+ ## 2. Sync Engine
31
+
32
+ ### 2.1 Sync Events Table
33
+ New table in `/data/mem.db` (or accounts.db):
34
+
35
+ ```sql
36
+ CREATE TABLE sync_events (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ namespace TEXT NOT NULL,
39
+ source_id TEXT NOT NULL,
40
+ action TEXT NOT NULL, -- create | update | delete
41
+ payload TEXT, -- JSON observation on create/update, null on delete
42
+ device_id TEXT, -- originating device (for echo filtering)
43
+ ingested_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
44
+ );
45
+
46
+ CREATE INDEX idx_sync_events_ns_cursor ON sync_events(namespace, id);
47
+ ```
48
+
49
+ Populated on every `POST /v1/ingest` and `POST /v1/documents/delete-batch` for Mem source types.
50
+
51
+ ### 2.2 Change Feed
52
+ **`GET /v1/sync/changes`**
53
+ - Query params: `cursor` (integer, last seen event ID), `namespace`, `limit` (default 50, max 1000)
54
+ - Response: `{ changes: [...], cursor: "new_id", has_more: bool }`
55
+ - Query: `WHERE namespace = ? AND id > ? ORDER BY id LIMIT ?`
56
+ - Namespace scoped via API key auth
57
+ - Server-side device_id echo filter (bandwidth optimisation, not correctness)
58
+ - Auth: requires `search` scope
59
+
60
+ ### 2.3 Client Fix: Pull Loop
61
+ Fix `engrm/src/sync/pull.ts` to loop on `has_more: true` for paginated backfill.
62
+
63
+ ---
64
+
65
+ ## 3. Namespace Model
66
+
67
+ - **Single namespace per team** — all members share it
68
+ - Solo users get personal namespace at signup (existing behaviour)
69
+ - `sensitivity: "shared"` → synced, visible to all team members
70
+ - `sensitivity: "personal"` → synced, server filters so only owner sees in search
71
+ - `sensitivity: "secret"` → never synced, local SQLite only
72
+
73
+ ### 3.1 Personal Visibility Enforcement (Security Blocker)
74
+ In `/v1/search`: when results come from a team namespace, inject server-side filter:
75
+ `(sensitivity != 'personal' OR metadata.user_id == <authed_user_id>)`
76
+
77
+ This is enforced in the search router based on the authenticated API key's account,
78
+ not by trusting client-side metadata filters.
79
+
80
+ ---
81
+
82
+ ## 4. Mem Provisioning
83
+
84
+ ### 4.1 Provision Endpoint
85
+ **`POST /v1/mem/provision`**
86
+ - Body: `{ code?: string, token?: string }` — exactly one required
87
+ - `code` = OAuth callback code (Flow A)
88
+ - `token` = provisioning token `cmt_...` (Flow C)
89
+ - Response: `{ api_key: "cvk_...", site_id, namespace, endpoint, user_id, teams: [] }`
90
+ - Server: validates code/token, creates or looks up account, creates namespace-scoped API key
91
+
92
+ ### 4.2 Device Code Flow (RFC 8628)
93
+ **`POST /v1/auth/device/code`**
94
+ - Response: `{ device_code, user_code: "XXXX-YYYY", verification_uri, interval: 5 }`
95
+ - Stores in `mem_device_codes` table with 15-minute expiry
96
+
97
+ **`POST /v1/auth/device/token`**
98
+ - Body: `{ device_code: "..." }`
99
+ - Response: `{ status: "pending" }` or `{ status: "complete", api_key: "cvk_...", ... }`
100
+ - Client polls at `interval` seconds, backs off on `slow_down`
101
+
102
+ ### 4.3 Token Revocation
103
+ **`POST /v1/auth/revoke`**
104
+ - Body: `{ api_key_prefix: "cvk_abc..." }` (first 12 chars for identification)
105
+ - Revokes the matching API key
106
+ - Auth: session cookie or service secret
107
+
108
+ ---
109
+
110
+ ## 5. Team Management
111
+
112
+ ### 5.1 Create Team
113
+ **`POST /v1/mem/teams`**
114
+ - Body: `{ name: "My Team" }`
115
+ - Creates namespace, team record, adds creator as owner
116
+ - Response: `{ id, name, namespace_id, role: "owner" }`
117
+
118
+ ### 5.2 List Teams
119
+ **`GET /v1/mem/teams`**
120
+ - Response: `[{ id, name, namespace_id, role, member_count }]`
121
+
122
+ ### 5.3 Team Detail
123
+ **`GET /v1/mem/teams/{id}`**
124
+ - Response: `{ id, name, namespace_id, members: [{ user_id, email, role, joined_at }] }`
125
+
126
+ ### 5.4 Invite
127
+ **`POST /v1/mem/teams/{id}/invite`**
128
+ - Body: `{ email?: string, role: "member"|"admin" }`
129
+ - Response: `{ invite_code, invite_url, expires_at }`
130
+ - Token-based (table-backed, revocable, single-use by default)
131
+ - Owner/admin only
132
+
133
+ ### 5.5 Join Team
134
+ **`POST /v1/mem/teams/{id}/join`**
135
+ - Body: `{ invite_code: "..." }`
136
+ - Grants existing API key access to team namespace (no new key)
137
+ - Response: `{ team_id, namespace_id }`
138
+ - Validates seat limit against billing plan
139
+
140
+ ### 5.6 Remove Member
141
+ **`DELETE /v1/mem/teams/{id}/members/{user_id}`**
142
+ - Query: `?action=keep|delete` (default: keep)
143
+ - `keep` = revoke namespace access, set `revoked_at`, observations stay
144
+ - `delete` = revoke access + async job to delete observations by `metadata.user_id`
145
+ - Owner/admin only, can't remove last owner
146
+ - Returns 202 for `action=delete` (async), 200 for `action=keep`
147
+
148
+ ### 5.7 Voluntary Leave
149
+ **`DELETE /v1/mem/teams/{id}/members/me`**
150
+ - Same as remove but self-targeted, no admin required
151
+ - Can't leave if last owner
152
+
153
+ ### 5.8 Update Team
154
+ **`PATCH /v1/mem/teams/{id}`**
155
+ - Body: `{ name?: string }`
156
+ - Owner/admin only
157
+ - Low priority
158
+
159
+ ---
160
+
161
+ ## 6. Billing & Access Control
162
+
163
+ ### 6.1 Mem Plan Limits
164
+
165
+ ```python
166
+ MEM_PLAN_LIMITS = {
167
+ "free": {"max_observations": 10_000, "max_devices": 2, "max_users": 1},
168
+ "solo": {"max_observations": 50_000, "max_devices": None, "max_users": 1},
169
+ "pro": {"max_observations": None, "max_devices": None, "max_users": 1},
170
+ "team": {"max_observations": None, "max_devices": None, "max_users": None, "min_seats": 3},
171
+ "enterprise": {"max_observations": None, "max_devices": None, "max_users": None},
172
+ }
173
+ ```
174
+
175
+ ### 6.2 Billing Endpoints
176
+
177
+ **`GET /v1/mem/plans`** (public, no auth)
178
+ - Returns plans with limits, pricing, currency, billing_period
179
+
180
+ **`POST /v1/mem/billing/checkout`**
181
+ - Body: `{ plan: "solo"|"pro"|"team", seats?: int }`
182
+ - Validates: team requires seats >= 3
183
+ - Creates Stripe checkout with `metadata.product = "mem"`
184
+ - Idempotency key: `{account_id}-{plan}-{timestamp_minute}`
185
+ - Response: `{ url }`
186
+
187
+ **`GET /v1/mem/billing`**
188
+ - Response:
189
+ ```json
190
+ {
191
+ "plan": "free",
192
+ "usage": { "observations": 8200, "devices": 2, "users": 1 },
193
+ "limits": { "max_observations": 10000, "max_devices": 2, "max_users": 1 },
194
+ "usage_pct": { "observations": 82, "devices": 100, "users": 100 },
195
+ "warn_at_pct": 80,
196
+ "upgrade_available": true,
197
+ "suggested_plan": "solo",
198
+ "upgrade_url": "https://engrm.dev/upgrade",
199
+ "current_period_end": "2026-04-11T00:00:00Z",
200
+ "trial_ends_at": null
201
+ }
202
+ ```
203
+
204
+ **`POST /v1/mem/billing/add-seats`**
205
+ - Body: `{ count: int }`
206
+ - Validates subscription is active/trialing
207
+ - Response: `{ total_seats, price_per_seat, new_monthly_total }`
208
+
209
+ **`POST /v1/mem/billing/remove-seats`**
210
+ - Body: `{ count: int }`
211
+ - Validates: can't go below 3, can't go below active member count
212
+ - 409 Conflict if validation fails
213
+ - Response: `{ total_seats, price_per_seat, new_monthly_total }`
214
+
215
+ **`GET /v1/mem/billing/portal`**
216
+ - Response: `{ url }` — Stripe customer portal, return_url → Mem dashboard
217
+
218
+ **`DELETE /v1/mem/billing`**
219
+ - Cancels subscription (cancel_at_period_end = true by default)
220
+ - Query: `?immediate=true` for immediate cancellation
221
+ - Response: `{ cancelled: true, access_until: "2026-04-11" }`
222
+
223
+ **`POST /v1/mem/billing/reactivate`**
224
+ - Un-cancel before period end
225
+ - Response: `{ reactivated: true }`
226
+
227
+ **`GET /v1/mem/billing/invoices`**
228
+ - Query: `?limit=10`
229
+ - Response: `[{ id, amount, currency, status, date }]`
230
+
231
+ ### 6.3 Enforcement Points
232
+
233
+ | Limit | Endpoint | Status | Response |
234
+ |---|---|---|---|
235
+ | Observation count | `POST /v1/ingest` | 402 | `{ error, limit, current, suggested_plan, upgrade_url }` |
236
+ | Device count | `POST /v1/mem/provision` | 402 | `{ error, limit, current }` |
237
+ | User/seat count | `POST /v1/mem/teams/{id}/join` | 402 | `{ error, seats_used, seats_total }` |
238
+ | Seat minimum | `POST /v1/mem/billing/remove-seats` | 409 | `{ error, min_seats: 3 }` |
239
+
240
+ Server-side observation count is authoritative (Qdrant count by namespace).
241
+ Client-side count is an approximation for UX only.
242
+
243
+ ### 6.4 Downgrade Handling
244
+ - Allow downgrade even if over new limit
245
+ - Block new syncs (402) until under limit
246
+ - Existing observations untouched
247
+ - Lifecycle (aging → archival) naturally reduces count
248
+ - Pinned observations still count toward quota
249
+
250
+ ### 6.5 Cancellation Handling
251
+ - Stripe `customer.subscription.deleted` → downgrade to free
252
+ - Existing data preserved, sync blocked above free limit
253
+
254
+ ---
255
+
256
+ ## 7. Usage & Activity
257
+
258
+ ### 7.1 User Usage
259
+ **`GET /v1/mem/usage`**
260
+ - Response: `{ observations_total, observations_this_month, syncs_this_month, by_project: [...] }`
261
+ - Derived from usage_meter counters (not Qdrant aggregation)
262
+
263
+ ### 7.2 Team Usage
264
+ **`GET /v1/mem/teams/{id}/usage`**
265
+ - Response: `{ total_observations, by_member: [{ user_id, count }], by_project: [{ canonical, count }] }`
266
+
267
+ ### 7.3 Activity Heatmap (GitHub-style graph)
268
+ **`GET /v1/mem/activity`**
269
+ - Query: `?days=365&team_id=optional`
270
+ - Response: `{ days: [{ date, count, by_type: { bugfix: 2, discovery: 3 } }] }`
271
+ - Aggregated from usage_meter counter table (increment on ingest), NOT Qdrant scroll
272
+
273
+ ### 7.4 Usage Counter Table
274
+
275
+ ```sql
276
+ CREATE TABLE mem_activity (
277
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
278
+ namespace TEXT NOT NULL,
279
+ user_id TEXT NOT NULL,
280
+ date TEXT NOT NULL, -- YYYY-MM-DD
281
+ obs_type TEXT NOT NULL, -- bugfix, discovery, etc.
282
+ count INTEGER DEFAULT 0,
283
+ UNIQUE(namespace, user_id, date, obs_type)
284
+ );
285
+
286
+ CREATE INDEX idx_mem_activity_ns_date ON mem_activity(namespace, date);
287
+ ```
288
+
289
+ Incremented on each `POST /v1/ingest` for Mem source types.
290
+ Heatmap query: `SELECT date, obs_type, SUM(count) FROM mem_activity WHERE namespace=? AND date>=? GROUP BY date, obs_type`
291
+
292
+ ---
293
+
294
+ ## 8. Data Management
295
+
296
+ ### 8.1 Export (Synchronous Streaming)
297
+ **`POST /v1/mem/export`**
298
+ - Body: `{ scope: "user"|"team"|"project", team_id?, project_canonical?, format: "jsonl" }`
299
+ - Response: streamed `application/x-ndjson` (chunked transfer encoding)
300
+ - Synchronous — no job queue needed for v1 data sizes
301
+
302
+ ### 8.2 Purge
303
+ **`POST /v1/mem/purge`**
304
+ - Body: `{ namespace, before_epoch, dry_run: bool, confirm: "DELETE" }`
305
+ - `dry_run: true` → `{ would_delete: int }`
306
+ - `dry_run: false` + `confirm: "DELETE"` → executes, `{ deleted: int }`
307
+ - Admin scope required
308
+ - Rate limited: 1 per namespace per 24 hours
309
+ - Logged to audit table
310
+
311
+ ### 8.3 On-Demand Compaction
312
+ **`POST /v1/mem/lifecycle/compact`**
313
+ - Triggers server-side compaction for caller's namespace
314
+ - Rate limited: once per 24 hours
315
+ - Helps users actively manage quota when near limit
316
+
317
+ ### 8.4 Device Management
318
+ **`POST /v1/mem/devices/{device_id}/revoke`**
319
+ - Marks device as revoked in `mem_devices`, frees slot
320
+ - Auth: account owner
321
+
322
+ ### 8.5 Import (Deferred to post-v1)
323
+
324
+ ---
325
+
326
+ ## 9. Database Schema
327
+
328
+ All Mem tables in `accounts.db` (same DB for foreign key integrity).
329
+
330
+ ```sql
331
+ -- Mem subscriptions (separate from Vector billing)
332
+ CREATE TABLE mem_subscriptions (
333
+ account_id TEXT PRIMARY KEY REFERENCES accounts(id),
334
+ plan TEXT NOT NULL DEFAULT 'free',
335
+ seats INTEGER DEFAULT 1,
336
+ stripe_subscription_id TEXT,
337
+ stripe_customer_id TEXT,
338
+ status TEXT DEFAULT 'active',
339
+ trial_ends_at TEXT,
340
+ cancel_at_period_end INTEGER DEFAULT 0,
341
+ created_at TEXT NOT NULL,
342
+ updated_at TEXT NOT NULL
343
+ );
344
+
345
+ -- Team management
346
+ CREATE TABLE mem_teams (
347
+ id TEXT PRIMARY KEY,
348
+ name TEXT NOT NULL,
349
+ namespace_id TEXT NOT NULL,
350
+ created_by TEXT NOT NULL REFERENCES accounts(id),
351
+ created_at TEXT NOT NULL
352
+ );
353
+
354
+ CREATE TABLE mem_team_members (
355
+ team_id TEXT NOT NULL REFERENCES mem_teams(id),
356
+ account_id TEXT NOT NULL REFERENCES accounts(id),
357
+ role TEXT NOT NULL DEFAULT 'member', -- owner | admin | member
358
+ joined_at TEXT NOT NULL,
359
+ revoked_at TEXT, -- set on removal with action=keep
360
+ PRIMARY KEY (team_id, account_id)
361
+ );
362
+
363
+ CREATE TABLE mem_invites (
364
+ id TEXT PRIMARY KEY,
365
+ team_id TEXT NOT NULL REFERENCES mem_teams(id),
366
+ invite_code TEXT UNIQUE NOT NULL,
367
+ email TEXT,
368
+ role TEXT NOT NULL DEFAULT 'member',
369
+ max_uses INTEGER DEFAULT 1, -- NULL = unlimited
370
+ use_count INTEGER DEFAULT 0,
371
+ created_by TEXT NOT NULL,
372
+ created_at TEXT NOT NULL,
373
+ expires_at TEXT NOT NULL,
374
+ used_by TEXT,
375
+ used_at TEXT
376
+ );
377
+
378
+ CREATE INDEX idx_invites_code ON mem_invites(invite_code);
379
+
380
+ -- Device tracking (for limit enforcement)
381
+ CREATE TABLE mem_devices (
382
+ id TEXT PRIMARY KEY,
383
+ account_id TEXT NOT NULL,
384
+ device_id TEXT NOT NULL,
385
+ device_name TEXT,
386
+ status TEXT DEFAULT 'active', -- active | revoked
387
+ registered_at TEXT NOT NULL,
388
+ last_seen_at TEXT NOT NULL,
389
+ UNIQUE(account_id, device_id)
390
+ );
391
+
392
+ -- Device code flow (RFC 8628)
393
+ CREATE TABLE mem_device_codes (
394
+ device_code TEXT PRIMARY KEY,
395
+ user_code TEXT UNIQUE NOT NULL,
396
+ account_id TEXT, -- NULL until approved
397
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | expired
398
+ poll_interval INTEGER DEFAULT 5,
399
+ created_at TEXT NOT NULL,
400
+ expires_at TEXT NOT NULL,
401
+ approved_at TEXT
402
+ );
403
+
404
+ -- Provisioning tokens
405
+ CREATE TABLE mem_provision_tokens (
406
+ token TEXT PRIMARY KEY, -- cmt_...
407
+ account_id TEXT NOT NULL REFERENCES accounts(id),
408
+ created_at TEXT NOT NULL,
409
+ expires_at TEXT NOT NULL,
410
+ used_at TEXT
411
+ );
412
+
413
+ -- Sync change feed
414
+ CREATE TABLE sync_events (
415
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
416
+ namespace TEXT NOT NULL,
417
+ source_id TEXT NOT NULL,
418
+ action TEXT NOT NULL, -- create | update | delete
419
+ payload TEXT, -- JSON on create/update, null on delete
420
+ device_id TEXT,
421
+ ingested_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
422
+ );
423
+
424
+ CREATE INDEX idx_sync_events_ns_cursor ON sync_events(namespace, id);
425
+
426
+ -- Activity heatmap counters
427
+ CREATE TABLE mem_activity (
428
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
429
+ namespace TEXT NOT NULL,
430
+ user_id TEXT NOT NULL,
431
+ date TEXT NOT NULL, -- YYYY-MM-DD
432
+ obs_type TEXT NOT NULL,
433
+ count INTEGER DEFAULT 0,
434
+ UNIQUE(namespace, user_id, date, obs_type)
435
+ );
436
+
437
+ CREATE INDEX idx_mem_activity_ns_date ON mem_activity(namespace, date);
438
+
439
+ -- Stripe webhook idempotency
440
+ CREATE TABLE stripe_events_processed (
441
+ event_id TEXT PRIMARY KEY,
442
+ processed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
443
+ );
444
+ ```
445
+
446
+ ---
447
+
448
+ ## 10. Stripe Integration
449
+
450
+ ### 10.1 Separate Products
451
+ Create distinct Stripe Products for Mem (not reuse Vector products):
452
+ - Engrm Solo ($9/mo)
453
+ - Engrm Pro ($19/mo)
454
+ - Engrm Team ($12/seat/mo)
455
+
456
+ All with `metadata.product = "mem"`.
457
+
458
+ ### 10.2 Webhook Routing
459
+ Separate endpoint: `POST /webhooks/stripe/mem`
460
+ - Own signing secret
461
+ - Idempotent (check `stripe_events_processed` table)
462
+ - Events: `checkout.session.completed`, `customer.subscription.updated`,
463
+ `customer.subscription.deleted`, `invoice.payment_succeeded`, `invoice.payment_failed`
464
+
465
+ ### 10.3 Checkout Metadata
466
+ Always include in subscription_data.metadata:
467
+ ```python
468
+ {
469
+ "product": "mem",
470
+ "plan": plan,
471
+ "account_id": account_id,
472
+ "seats": seats
473
+ }
474
+ ```
475
+
476
+ ---
477
+
478
+ ## 11. Security Requirements
479
+
480
+ 1. **Personal visibility**: Server-side filter on `/v1/search` — `(sensitivity != 'personal' OR user_id == authed_user)`
481
+ 2. **Quota authority**: Server-side Qdrant count is authoritative, not client SQLite count
482
+ 3. **Clock manipulation**: Use server `ingested_at` for lifecycle, not client `created_at_epoch`
483
+ 4. **Rate limits**: 5 checkout sessions per account per hour
484
+ 5. **Idempotency keys**: On all Stripe checkout creation
485
+ 6. **Seat integrity**: `remove-seats` validates `remaining >= active_member_count`
486
+ 7. **Bearer prefix validation**: Reject non-`cvk_` tokens before DB lookup
487
+ 8. **Scrubber patterns**: Add `stripe_customer_id`, `stripe_subscription_id` to secret scrubber
488
+
489
+ ---
490
+
491
+ ## 12. Implementation Priority
492
+
493
+ ### Phase 3.0 — Sync Blockers
494
+ 1. Bearer auth support in `get_auth_context()`
495
+ 2. Client search param fix (`q`/`top_k` → `query`/`limit`)
496
+ 3. `sync_events` table + `GET /v1/sync/changes`
497
+ 4. `POST /v1/documents/delete-batch`
498
+ 5. `POST /v1/mem/provision`
499
+ 6. Personal visibility filter in `/v1/search`
500
+
501
+ ### Phase 3.1 — Auth Flows
502
+ 7. `POST /v1/auth/device/code` + `/v1/auth/device/token`
503
+ 8. `POST /v1/auth/revoke`
504
+
505
+ ### Phase 3.2 — Team Management
506
+ 9. Teams CRUD (create, list, detail)
507
+ 10. Invite + join
508
+ 11. Remove member + voluntary leave
509
+
510
+ ### Phase 3.3 — Billing
511
+ 12. `mem_subscriptions` table + `GET /v1/mem/plans`
512
+ 13. Checkout + billing status
513
+ 14. Add/remove seats
514
+ 15. Stripe webhook handler (`/webhooks/stripe/mem`)
515
+ 16. Quota enforcement on ingest + provision
516
+
517
+ ### Phase 3.4 — Observability
518
+ 17. `GET /v1/mem/usage` + `GET /v1/mem/teams/{id}/usage`
519
+ 18. `GET /v1/mem/activity` (heatmap)
520
+ 19. `mem_activity` counter table
521
+
522
+ ### Phase 3.5 — Data Management
523
+ 20. `POST /v1/mem/export` (streaming)
524
+ 21. `POST /v1/mem/purge` (with dry_run + confirm)
525
+ 22. `POST /v1/mem/lifecycle/compact`
526
+ 23. `POST /v1/mem/devices/{id}/revoke`
527
+
528
+ ### Deferred (post-v1)
529
+ - Import endpoint
530
+ - Dashboard graph visualization
531
+ - Team rename
532
+ - Annual billing
533
+ - Enterprise SSO
534
+
535
+ ---
536
+
537
+ ## 13. File Structure (candengo-vector)
538
+
539
+ ```
540
+ app/
541
+ ├── routers/
542
+ │ ├── mem.py # /v1/mem/* — teams, usage, activity, export, purge
543
+ │ ├── mem_billing.py # /v1/mem/billing/* — checkout, seats, portal
544
+ │ ├── mem_provision.py # /v1/mem/provision, /v1/auth/device/*
545
+ │ └── sync.py # /v1/sync/changes
546
+ ├── services/
547
+ │ ├── mem_teams.py # Team CRUD, invite, join, remove
548
+ │ ├── mem_billing.py # Plan limits, subscription management
549
+ │ ├── mem_devices.py # Device registration, tracking, revocation
550
+ │ ├── mem_activity.py # Activity counters, heatmap queries
551
+ │ ├── mem_provision.py # Provisioning token + device code logic
552
+ │ └── sync_events.py # Sync event log, change feed queries
553
+ ```