Storage¶
Engine¶
SQLAlchemy Core over a URL you choose:
| Environment | VENKAI_DATABASE_URL |
|---|---|
| Development | unset → local SQLite file (zero setup) |
| Production | postgresql://user:pass@host/db |
SQLite gets check_same_thread=False and pragma tuning at connection time.
Nothing in the schema is Postgres-specific, so the two are interchangeable —
which is why the test suite can run against SQLite and mean something.
Entities¶
erDiagram
ORGANIZATION ||--o{ USER : has
ORGANIZATION ||--o{ API_KEY : has
ORGANIZATION ||--o{ PROJECT : owns
PROJECT ||--o{ CONTEXT : contains
CONTEXT ||--o{ VERSION : "has history"
ORGANIZATION { string id PK string name }
PROJECT { string id PK string org_id FK string project_key string name string status }
CONTEXT {
string id PK
string org_id FK
string project_id FK
string agent_key
string type
text content
float confidence
float importance
int access_count
float last_accessed_at
float created_at
float updated_at
text metadata_json
text embedding_json
}
VERSION { string id PK string context_id FK int version text content }
The contexts table¶
| Column | Type | Notes |
|---|---|---|
id |
string | ctx_ + 12 hex |
org_id |
string | Denormalised onto the row — this is what the second org check reads |
project_id |
string | Internal proj_… id, never exposed |
agent_key |
string | The agent_id from the write |
type |
string | One of six, validated at the API boundary |
content |
text | 1–8000 chars |
confidence |
float | Stored, unused in ranking |
importance |
float | 25 % of the retrieval score |
access_count |
int | Not incremented by retrieval |
last_accessed_at |
float | Nullable |
created_at / updated_at |
float | Epoch seconds |
metadata_json |
text | JSON |
embedding_json |
text | JSON float array |
org_id being on the context row rather than reached through the project join
is deliberate: it makes the per-candidate ownership check a field comparison
instead of a second query.
Embeddings are JSON text¶
Not pgvector, not BYTEA, not a native array. Cosine similarity is computed
in Python over the loaded candidate set.
This is a stated, reversible choice. The docstring in the retriever explains
the exit: the column is a 1:1 match for a pgvector Vector column with
identical cosine semantics, so migrating is a type change plus a backfill —
not a redesign. pgvector was explicitly removed from the dependency set
because nothing imported it and no migration declared CREATE EXTENSION
vector; carrying an unused dependency that suggests an index exists is worse
than not having it.
What it costs¶
hashing (128-d) |
semantic (384-d) |
|
|---|---|---|
| Per memory | ~2.5 KB of JSON text | ~7.5 KB |
| 10 000 memories | ~25 MB | ~75 MB |
| 100 000 memories | ~250 MB | ~750 MB |
Several times a packed binary vector. Fine at current scale, and a real number to plan against beyond it.
Project identity¶
flowchart LR
K["project_key<br/>'billing-service'<br/><i>client-chosen</i>"] -->|"resolved per request"| I["id<br/>'proj_878b0d7ab961'<br/><i>internal</i>"]
I --> R["context rows"]
R -->|"substituted on every response"| K
Two resolution modes:
| Path | Behaviour on unknown key |
|---|---|
Write (POST /api/memory) |
Creates the project |
Read (GET /api/context/…) |
404 |
The internal id never leaves the API — every response substitutes your key back. A client therefore cannot couple to it, and the internal id stays free to change.
The asymmetry is also the source of the most common integration bug: a typo on the write path succeeds silently.
Isolation¶
flowchart TB
R["request"] --> A["auth → org_id"]
A --> Q["query WHERE project org-keyed"]
Q --> C["candidate rows"]
C --> V{"row.org_id == caller.org_id?"}
V -->|no| D["discarded"]
V -->|yes| K["scored"]
Two independent barriers. The second is redundant with the first by design —
a stale project id, a caching mistake, or one missed WHERE clause cannot leak
another tenant's data through it.
Migrations¶
Alembic, configured in venkai/alembic.ini.
Run before starting a new version. There is no automatic migration on startup — a deployment that silently migrates is a deployment that can silently migrate the wrong way.
Operational reality¶
| Backups | Not provided. Your database, your policy. |
| Retention / TTL | None. Memories persist until edited or the org is deleted. |
| Per-memory delete | Does not exist. PATCH to supersede. |
| Bulk export | GET /api/export |
| Bulk delete | DELETE /api/organization — irreversible |
| Docker volume | venkai-data → /data |
Related¶
- Persistence — lifecycle and versioning
- Embeddings · Production