DB schema
The persistent shape of Agentfy.ai 2.0. Prisma is the repository (no extra repository layer); one Postgres database, models grouped by the same layered slices.
api/prisma/schema.prisma is committed, and it is a generated file — its own first line says so. Each slice keeps its models in a <slice>.prisma at its root and the schema is assembled from them, which has two consequences worth knowing before you edit anything: change the slice file, never the assembly; and generate with bun run prisma:generate, because a bare npx prisma generate assembles nothing and exits zero against whatever is already on disk, handing you a client that does not know your new model. Snippets below are illustrative; the source of truth is the per-slice files.
Conventions
- IDs are
{slice}-{uuid}(e.g.agent-3f9c…,team-a17f…), generated in the slice'smapper.toCreate— readable and self-describing. - Tenancy: almost every row carries a
teamId(theteam= the tenant / the store). Agent-domain rows carry anagentId, which resolves to a team. - Timestamps:
createdAt/updatedAton every model; soft-delete (deletedAt) only where history matters. - JSON for open shapes: agent
config, taskpayload, channelsettings— typed in code,Jsonin the DB. - Secrets are never plaintext — see the envelope-encrypted
Secretmodel below and Agent secrets.
Entities by layer
user Team ─┬─ UserTeam ─ User apiKey · invite · role
└─ (planId ← billing)
agent Agent ─┬─ Memory ← agent = the row that IS the agent
├─ KnowledgeBase ─ Document
├─ Chat ─ Message
├─ Channel · Integration · Skill · Cron · Access
└─ Secret (envelope-encrypted)
runtime Task ─ AgentRuntimeSession ─ Event ← ephemeral execution
system Usage · Setting · Notification · File ← shared services
admin AuditLog · FeatureFlag
billing Subscription ─ Price ─ Product · Invoice ─ Payment · PaymentMethod · WebhookEventEverything hangs off Team (tenant) at the top and Agent (the agent identity) in the middle.
Core models (illustrative)
Tenancy — user
model Team {
id String @id // team-{uuid}
name String
planId String? // written by billing; read by system/setting (inverted dep)
members UserTeam[]
agents Agent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id String @id // user-{uuid}
email String @unique
name String?
teams UserTeam[]
createdAt DateTime @default(now())
}
model UserTeam { // membership + role
id String @id
userId String
teamId String
role String // owner | admin | member
user User @relation(fields: [userId], references: [id])
team Team @relation(fields: [teamId], references: [id])
@@unique([userId, teamId])
}The agent — agent
model Agent {
id String @id // agent-{uuid} — this row IS the agent
teamId String
name String
status String @default("active") // active | disabled | archived
type String @default("standard") // standard | concierge — server-assigned only
soul String? // SOUL.md · USER.md · HEARTBEAT.md — the agent's own text,
user String? // stored verbatim, exported and re-imported byte for byte
heartbeat String?
config Json @default("{}") // the runtime-compatible agent.config.json
runtimeProfile String @default("none") // may it have hands, and how much machine
promptLogEnabled Boolean @default(false) // record this agent's assembled prompts
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}Everything that names an agentId cascades from this row — conversations, memory, skills, secrets, autostart config, activity. A cascade is one statement in one transaction, so a delete interrupted half-way removes nothing; an explicit sweep of seven tables could stop between any two of them and leave exactly the orphans the cascade exists to prevent. The one thing outside it is VectorEmbedding, which carries no foreign key by design and is swept explicitly.
See Anatomy for what each column means.
Envelope-encrypted secrets — agent/secret
model Secret {
id String @id // secret-{uuid}
agentId String
name String // e.g. OPENCART_API_KEY
ciphertext Bytes // AES-256-GCM payload (the wrapped value)
iv Bytes // per-record nonce
authTag Bytes // GCM auth tag (AEAD)
keyVersion Int // which KEK wrapped the DEK
agent Agent @relation(fields: [agentId], references: [id])
createdAt DateTime @default(now())
@@unique([agentId, name])
}The KEK never leaves api; the worker pod never sees plaintext. See Agent secrets.
Ephemeral execution — runtime
model AgentRuntimeSession {
id String @id // session-{uuid}
agentId String
taskId String?
status String // pending|starting|running|idle|stopping|stopped|failed
failureReason String? // dial_timeout | handshake_timeout | ready_timeout |
// session_mismatch | channel_version | heartbeat_lost
runtimeType String // none | light | browser | heavy | warm
cpuLimit String?
memLimit String?
storageLimit String?
ttlSeconds Int
k8sJobName String?
namespace String?
workerUrl String?
logsUrl String?
startedAt DateTime?
lastActivityAt DateTime?
stoppedAt DateTime?
@@index([agentId, status])
}(See the Worker lifecycle for the state machine.)
The rest, by layer
| Layer | Models (sketch) |
|---|---|
| user | ApiKey { teamId, hash, scopes } · Invite { teamId, email, role, token, expiresAt } · Role |
| system | Usage { teamId, agentId?, kind, amount, unit, ts } · Setting { scope, key, value } · Notification { userId, type, readAt } · File { teamId, agentId?, storageKey, name, size, mime, tags } |
| admin | AuditLog { actorId, action, target, meta, ts } · FeatureFlag { key, enabled, scope } |
| runtime | Task { teamId, agentId, kind, status, payload, attempts } · Event { sessionId?, type, data } (mostly streamed via Redis; persisted only for audit) |
| agent | Memory { agentId, kind, content, embeddingRef? } · KnowledgeBase { agentId, workspace } ─ Document · Chat { agentId, channel } ─ Message { chatId, role, content } · Channel · Integration · Skill · Cron · Access |
| billing | Subscription { teamId, priceId, status, currentPeriodEnd } · Product ─ Price · Invoice ─ Payment · PaymentMethod · WebhookEvent |
Where non-relational state lives (not in this schema)
- Vectors + knowledge graph → Postgres pgvector + Apache AGE, owned by LightRAG per
workspace(one perKnowledgeBase). See LightRAG. - Queue + pub/sub + cache → Redis (BullMQ jobs,
eventsstream). Not durable truth. - Object storage → files/artifacts by reference; the
Filerow holds thestorageKey, the bytes live in S3/R2.
See also
- Layered slices · Slice breakdown — where each model's slice sits.
- Runtime model — how
Task/AgentRuntimeSessionmove per turn. - Agent secrets — the envelope-encryption scheme behind
Secret.