Skip to content

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'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 } · ProductPrice · 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 + cacheRedis (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