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.
- package/.mcp.json +9 -0
- package/AUTH-DESIGN.md +436 -0
- package/BRIEF.md +197 -0
- package/CLAUDE.md +44 -0
- package/COMPETITIVE.md +174 -0
- package/CONTEXT-OPTIMIZATION.md +305 -0
- package/INFRASTRUCTURE.md +252 -0
- package/LICENSE +105 -0
- package/MARKET.md +230 -0
- package/PLAN.md +278 -0
- package/README.md +121 -0
- package/SENTINEL.md +293 -0
- package/SERVER-API-PLAN.md +553 -0
- package/SPEC.md +843 -0
- package/SWOT.md +148 -0
- package/SYNC-ARCHITECTURE.md +294 -0
- package/VIBE-CODER-STRATEGY.md +250 -0
- package/bun.lock +375 -0
- package/hooks/post-tool-use.ts +144 -0
- package/hooks/session-start.ts +64 -0
- package/hooks/stop.ts +131 -0
- package/mem-page.html +1305 -0
- package/package.json +30 -0
- package/src/capture/dedup.test.ts +103 -0
- package/src/capture/dedup.ts +76 -0
- package/src/capture/extractor.test.ts +245 -0
- package/src/capture/extractor.ts +330 -0
- package/src/capture/quality.test.ts +168 -0
- package/src/capture/quality.ts +104 -0
- package/src/capture/retrospective.test.ts +115 -0
- package/src/capture/retrospective.ts +121 -0
- package/src/capture/scanner.test.ts +131 -0
- package/src/capture/scanner.ts +100 -0
- package/src/capture/scrubber.test.ts +144 -0
- package/src/capture/scrubber.ts +181 -0
- package/src/cli.ts +517 -0
- package/src/config.ts +238 -0
- package/src/context/inject.test.ts +940 -0
- package/src/context/inject.ts +382 -0
- package/src/embeddings/backfill.ts +50 -0
- package/src/embeddings/embedder.test.ts +76 -0
- package/src/embeddings/embedder.ts +139 -0
- package/src/lifecycle/aging.test.ts +103 -0
- package/src/lifecycle/aging.ts +36 -0
- package/src/lifecycle/compaction.test.ts +264 -0
- package/src/lifecycle/compaction.ts +190 -0
- package/src/lifecycle/purge.test.ts +100 -0
- package/src/lifecycle/purge.ts +37 -0
- package/src/lifecycle/scheduler.test.ts +120 -0
- package/src/lifecycle/scheduler.ts +101 -0
- package/src/provisioning/browser-auth.ts +172 -0
- package/src/provisioning/provision.test.ts +198 -0
- package/src/provisioning/provision.ts +94 -0
- package/src/register.test.ts +167 -0
- package/src/register.ts +178 -0
- package/src/server.ts +436 -0
- package/src/storage/migrations.test.ts +244 -0
- package/src/storage/migrations.ts +261 -0
- package/src/storage/outbox.test.ts +229 -0
- package/src/storage/outbox.ts +131 -0
- package/src/storage/projects.test.ts +137 -0
- package/src/storage/projects.ts +184 -0
- package/src/storage/sqlite.test.ts +798 -0
- package/src/storage/sqlite.ts +934 -0
- package/src/storage/vec.test.ts +198 -0
- package/src/sync/auth.test.ts +76 -0
- package/src/sync/auth.ts +68 -0
- package/src/sync/client.ts +183 -0
- package/src/sync/engine.test.ts +94 -0
- package/src/sync/engine.ts +127 -0
- package/src/sync/pull.test.ts +279 -0
- package/src/sync/pull.ts +170 -0
- package/src/sync/push.test.ts +117 -0
- package/src/sync/push.ts +230 -0
- package/src/tools/get.ts +34 -0
- package/src/tools/pin.ts +47 -0
- package/src/tools/save.test.ts +301 -0
- package/src/tools/save.ts +231 -0
- package/src/tools/search.test.ts +69 -0
- package/src/tools/search.ts +181 -0
- package/src/tools/timeline.ts +64 -0
- 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
|
+
```
|