Skip to content

DB schema ​

Historical design content: references below to a planned LightRAG/graph backend are superseded by AGNT2-303. Current knowledge uses word/vector retrieval; those references are not implementation or deployment instructions. Knowledge.

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.

There is no assembled schema (Prisma 8, AGNT2-540). Each slice keeps its models in a <slice>.prisma at its root, and api/prisma.config.ts globs them all — edit the slice file, there is nothing else to edit. bun run prisma:emit turns them into the contract the client is typed from (api/generated/prisma, generated and gitignored; build, start:dev, postinstall and the test setup emit it for you). A schema change is a migration: emit, bun run db:plan -- --name <what>, review the TypeScript plan it writes under api/prisma/migrations/app/, move the deploy ref, then bun run db:deploy — the steps are in api/src/slices/infra/prisma/README.md. 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's mapper.toCreate — readable and self-describing.
  • Tenancy: almost every row carries a teamId (the team = the tenant / the store). Agent-domain rows carry an agentId, which resolves to a team.
  • Timestamps: createdAt / updatedAt on every model; soft-delete (deletedAt) only where history matters.
  • JSON for open shapes: agent config, task payload, channel settings — typed in code, Json in the DB.
  • Secrets are never plaintext — see the envelope-encrypted Secret model 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 · WebhookEvent

Everything hangs off Team (tenant) at the top and Agent (the agent identity) in the middle.

Core models (illustrative) ​

Tenancy — user ​

prisma
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 ​

prisma
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 ​

prisma
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 ​

prisma
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 ​

LayerModels (sketch)
userApiKey { teamId, hash, scopes } · Invite { teamId, email, role, token, expiresAt } · Role
systemUsage { teamId, agentId?, kind, amount, unit, ts } · Setting { scope, key, value } · Notification { userId, type, readAt } · File { teamId, agentId?, storageKey, name, size, mime, tags }
adminAuditLog { actorId, action, target, meta, ts } · FeatureFlag { key, enabled, scope }
runtimeTask { teamId, agentId, kind, status, payload, attempts } · Event { sessionId?, type, data } (mostly streamed via Redis; persisted only for audit)
agentMemory { agentId, kind, content, embeddingRef? } · KnowledgeBase { agentId, workspace } ─ Document · Chat { agentId, channel } ─ Message { chatId, role, content } · Channel · Integration · Skill · Cron · Access
billingSubscription { 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 per KnowledgeBase). See LightRAG.
  • Queue + pub/sub + cache → Redis (BullMQ jobs, events stream). Not durable truth.
  • Object storage → files/artifacts by reference; the File row holds the storageKey, the bytes live in S3/R2.

See also ​