Architecture

Prisma Architecture: How It Actually Works

prisma/prisma Apache-2.0 1 diagram
How to read this page

Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. The terms:

Module (or cluster)
A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
Modularity (the Q number)
A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
Hub
The most depended-upon symbol inside one module.
Load-bearing symbols
PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
Arrows and their numbers
How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
Dependency cycle
File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
Mutually recursive symbols
Functions that call each other, usually the natural shape of parsers and tree-walking code.

Prisma Next is a TypeScript rewrite of Prisma ORM, in Early Access on the default branch of prisma/prisma while Prisma ORM 7 continues from the repository's v7 branch. It moves the schema off the codegen path and onto a contract-first model: a .prisma file compiles to a versioned JSON contract plus TypeScript types, and queries are written against a composable DSL that compiles to SQL at runtime, described in the repo's own ARCHITECTURE.md. Symvanta indexed the monorepo and grouped its symbols into 500 functional clusters (modularity Q=0.96); the map flags a display cap at that number, so a monorepo laid out in ten numbered package groups carries more clusters than the map prints. The largest, 1,217 symbols anchored on AnyExpression, is the SQL expression AST every query plan is built from, and the ORM client that builds those plans calls into it 67 times while it calls back 62 times, the heaviest pair of edges on the map.

Module map

The diagram shows the 10 largest of 500 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them, the kind of connectivity data a code graph captures that similarity search alone can't. Two pairs dominate it. SQL Relational AST and SQL ORM Client Collections trade 67 and 62 calls, the seam where a user's db.orm.User.all() becomes an expression tree. SQL Storage Contract IR and SQL Schema IR trade 13 and 7, the two representations of a database's structure that the contract compiler and the migration planner each work from. Integration-test clusters are kept off the diagram: Postgres Port Test Harness (347 symbols, hub withPostgresPort), Mongo Port Test Harness (333, hub withMongoPort), and Engine Command Test Harness (398, hub JourneyContext) all live under test/integration/, and they group by shared fixture scaffolding. Two production clusters just missed the top ten and are worth knowing about: Symbol Table Management (384 symbols, hub interpretPslDocumentToSqlContract), which interprets a parsed PSL document into a SQL contract, and Postgres Migration Tools (244), whose 14 calls into SQL Storage Contract IR are the heaviest arrow not drawn.

prisma/prisma module map: the 10 largest of 500 detected modules with call-weighted edges, generated by Symvanta
Module map of prisma/prisma, generated by Symvanta. Link to this diagram Open full size

Where to start reading

These are 12 of the codebase's most depended-upon symbols, blended from the global PageRank ranking and the hub of each of the ten largest production modules. Four raw candidates are swapped out. withPostgresPort and withMongoPort rank first and sixth, and both are integration-test port harnesses under test/integration/test/ports/_harness/: every port test in the repo opens its database through one of them. defineConfig resolves to a build-config stub from the external tsdown package rather than to prisma source. createOrmClient lives in examples/paradedb-demo, a sample app rather than a shipped package. Start here to understand how the pieces connect.

A few of these are worth calling out individually. AnyExpression is the union every SQL expression node satisfies, and ColumnRef.of is the constructor that turns a table and column pair into the leaf those expressions are built from; both live in one 2,328-line ast/types.ts that the whole SQL family compiles against. SqlStorage is the contract's picture of a database: namespaces, tables, and the type entries normaliseTypeEntry canonicalises. Collection is the type application code holds behind db.orm.User, CollectionImpl intersected with the aggregate reducers that model's contract declares. CliStructuredError.is is the type guard every CLI command runs on a caught error before deciding what to print, and MigrationToolsError is the migration package's own error class carrying a category. SyntaxNode.children walks the PSL parser's red tree. postgresError and its sibling sqliteError build the target-specific error envelopes each database extension throws. serializeValue renders a contract value back into emitted TypeScript source, and MigrationCLI.run (packages/1-framework/3-tooling/cli/src/migration-cli.ts) is the static entry point a migration module runs itself through, returning the process exit code.

Key subsystems

SQL Relational AST

The largest cluster at 1,217 symbols: the query AST the SQL family shares, under packages/2-sql/4-lanes/relational-core/src/ast/. AnyExpression, Expression, AstNode, and ColumnRef define the node types; AstNode.freeze and frozenArrayCopy make every node immutable once built, which is what lets one plan be inspected, rewritten by middleware, and rendered without copying. Its 62 calls into the ORM client are the return leg of the pair described above, and it reaches into a smaller Expression Handling cluster 29 more times.

SQL ORM Client Collections

472 symbols in packages/3-extensions/sql-orm-client/, the layer application code touches. CollectionImpl carries the chainable query API (.where(), .include(), .all()) and Collection is the public type over it, resolveModelTableName and domainModelTableInNamespace map a contract model onto its physical table, and JunctionThrough carries many-to-many relations. ormError, OrmCode, and OrmSubcode give every failure here a structured code. This package also holds the repo's largest dependency cycle, 20 files (see Health signals below).

SQL Storage Contract IR

735 symbols in packages/2-sql/1-core/contract/src/ir/: the contract's own model of storage. SqlStorage, SqlNamespace, SqlNamespaceEntries, and StorageTable describe what exists in the database, and PostgresSchema, PostgresTableSchemaNode, and PostgresDatabaseSchemaNode specialise that for Postgres. Everything downstream reads the contract through this cluster, which is why Postgres Migration Tools calls into it 14 times and SQL Schema IR 7 more.

SQL Schema IR

509 symbols in packages/2-sql/1-core/schema-ir/, a second representation aimed at migrations: SqlTableIR, SqlColumnIR, SqlSchemaIR, plus the constraint nodes SqlCheckConstraintIR, SqlForeignKeyIR, SqlUniqueIR, and SqlIndexIR. The hub is defineNonEnumerable, a twelve-line helper the IR nodes use to attach a derivation-time field that stays out of JSON.stringify, out of structural test assertions, and out of spreads, while the one consumer that needs it at plan time still reads it as node.field. The contract IR and the schema IR call each other 13 and 7 times, the second-heaviest pair on the map.

PSL Syntax Tree

615 symbols in packages/1-framework/2-authoring/psl-parser/src/syntax/, a red/green syntax tree of the sort rust-analyzer and Roslyn use: immutable GreenNode and GreenElement values hold the shape and text, and the red layer (SyntaxNode, SyntaxToken, SyntaxElement) wraps them with absolute offsets and parent links computed on demand. SyntaxNode.children, findChildToken, and SourceFile.positionAt are how the CLI and the language server navigate a .prisma file. The cluster has almost no outbound weight (three calls total) because a syntax tree is read by everything and calls almost nothing.

Mongo Aggregation Expressions

522 symbols in packages/2-mongo-family/4-query/query-ast/. MongoAggExpr, MongoAggExprNode, MongoFilterExpr, MongoFieldFilter, and MongoAggOperator.of are the MongoDB half of the same idea the SQL AST implements: a typed expression tree that a target lowers into a driver command. It calls into the pipeline-stage cluster (hub MongoStageNode) 11 times, and that cluster calls back 15 times, since a $match stage holds a filter expression and a filter expression is assembled inside a stage. The whole 2-mongo-family group repeats the 2-sql layering, with its own foundation, authoring, tooling, query, transport, and runtime tiers.

Postgres Target and DDL

545 symbols spanning the Postgres target, the postgres extension, and the SQL family's control adapter. PostgresDdlNode, PostgresDdlVisitor, AlterTableAction, and quoteIdentifier are the DDL side, the nodes a migration plan renders into CREATE TABLE and ALTER TABLE; ExecuteRequestLowerer.lowerToExecuteRequest lowers a statement into the shape the driver executes; and postgresError with PostgresTargetErrorCode shapes what surfaces when Postgres rejects it. The same three-way split (target, adapter, driver) repeats under packages/3-targets/ for SQLite, and Database Query Utilities (315 symbols, hub sqliteError) is its SQLite twin, calling into this cluster 12 times.

CLI Errors and Command Actions

649 symbols across packages/1-framework/1-core/errors/ and the CLI that consumes it. CliStructuredError.is and CliStructuredError.code classify a caught error, normalizeError puts anything thrown into that shape, and ActionableCliError with ActionableCliError.nextActions carries the remediation steps chooseAction and runCommandAction print. Its heaviest edge, 12 calls into Migration Tools, is the CLI invoking the migration engine; Migration Tools calls back 5 times to raise its own errors through the same reporting path.

Also on the map

Two more of the ten drawn modules are CLI-side. Migration Tools (401 symbols, hub MigrationToolsError) lives in packages/1-framework/3-tooling/migration/ and assembles contract spaces: createAggregateContractSpace, makeAggregateContractSpace, and createContractSpaceAggregate combine the per-package contracts of a workspace into the single view a migration plans against. CLI Migration Output Rendering (398, hub toneSpans) is everything the terminal shows while that runs: toneSpans and toneDrawing parse the CLI's inline tone markup, ClassifiedEdge and the renderers under src/utils/formatters/ draw the migration graph, and shortDisplayHash abbreviates a contract hash for display.

Canonical request flow

Prisma Next exposes no HTTP surface of its own (the endpoint scan over prisma/prisma returns nothing), so the flow traced here is the read path every query takes: what happens between db.orm.User.all() and the SQL text that reaches the driver.

  1. CollectionImpl.all (packages/3-extensions/sql-orm-client/src/collection.ts#L1025) is the read terminal. It takes an optional configure callback for typed annotations, folds them into the collection's state, and calls the private dispatch.
  2. CollectionImpl.#dispatch (packages/3-extensions/sql-orm-client/src/collection.ts#L2492) packs the accumulated builder state into one options object: the execution context, the runtime, the collection state, and the table, model, and namespace names.
  3. dispatchCollectionRows (packages/3-extensions/sql-orm-client/src/collection-dispatch.ts#L75) branches on whether the query has includes. With none, it compiles and runs one select; with includes, dispatchWithIncludes lowers every include descriptor into correlated subqueries so the read path still issues a single query.
  4. compileSelect (packages/3-extensions/sql-orm-client/src/query-plan-select.ts#L1451) turns that state into a plan. It resolves polymorphism against the contract, builds the projection and any table-inheritance joins, assembles a SelectAst, and derives the parameter list from the ParamRef nodes inside it.
  5. queryPlanRows (packages/3-extensions/sql-orm-client/src/query-plan-rows.ts#L5) is a six-line seam: it hands the plan to scope.query(plan), where scope is a RuntimeScope, the two-method interface sql-relational-core owns so the ORM client and the runtime share one contract without a layering inversion.
  6. SqlRuntimeBase.query (packages/2-sql/5-runtime/src/sql-runtime.ts#L309) is the implementation behind that interface. It forwards to queryAgainstQueryable, which opens an async generator, prepares the plan, and streams decoded rows back.
  7. SqlRuntimeBase.lowerToDraft (packages/2-sql/5-runtime/src/sql-runtime.ts#L254) runs inside that preparation. It produces a draft with SQL rendered and params filled from the user-domain values the lowering collected from ParamRef nodes. No codec encoding has happened yet, which is the window where a middleware can still mutate those params through the SqlParamRefMutator.
  8. lowerSqlPlan (packages/2-sql/5-runtime/src/lower-sql-plan.ts#L16) calls adapter.lower(ast, { contract, params }), unwraps the returned literal slots into a bare value array, and freezes the result. A bind-site slot arriving here means the caller sent a prepared-statement AST down the ad-hoc path, and it raises RUNTIME.PREPARE_BIND_ON_ADHOC.
  9. PostgresAdapterImpl.lower (packages/3-targets/6-adapters/postgres/src/core/adapter.ts#L85) is the concrete adapter behind that interface call. It refuses DDL, which belongs to the control adapter, and delegates the rest with its codec registry attached.
  10. renderLoweredSql (packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts#L151) walks the AST and emits Postgres-flavoured { sql, params }. It collects the ordered ParamRef nodes first, assigns each a $n index, then renders. The runtime and control entry points share this one function so an emitted migration and a live query produce byte-identical SQL for the same AST.

Two of those steps go through an interface rather than a direct call, and both are the extension points the rewrite is built around. Step 5 into step 6 crosses RuntimeScope, which is how the same ORM client drives a plain connection, a pooled one, or a test double. Step 8 into step 9 crosses Adapter.lower, which is how one AST reaches Postgres, SQLite, or any adapter a third party ships: SqliteAdapterImpl implements the identical pair of lower and renderLoweredSql in packages/3-targets/6-adapters/sqlite/src/core/adapter.ts.

Health signals

Symvanta detected 27 dependency cycles across 500 modules (modularity Q=0.96). The largest cycle spans 20 files in the sql-orm-client area: collection-column-mapping.ts, aggregate-builder.ts, grouped-collection.ts, query-plan.ts, filters.ts, and their siblings all reference each other while building one query plan, which is what a builder API with chainable, mutually-referencing stages produces. The suggested break edge runs from collection-column-mapping.ts into collection-contract.ts. The remaining 26 cycles are small: the next largest are 7 files each in mongo-schema-ir and in the SQL runtime's prepared/ directory, then 6 in the Mongo contract IR. A Q of 0.96 across 500 clusters is the shape a monorepo with hard layering rules produces, and this repo writes those rules down: a dependency-cruiser.config.mjs and an architecture.config.json sit at its root, next to the ARCHITECTURE.md that states the dependency direction.

54 sets of mutually recursive symbols were also detected, the largest being ast (37 symbols): the expression node types in relational-core that reference each other by construction, since a BinaryExpr holds two Expression values and an Expression may be a BinaryExpr. The next two are the per-adapter SQL renderers, 26 symbols and 23 symbols, where renderWindowFuncExpr, renderJoinOn, renderCastExpr, and two dozen siblings call back into the shared renderParts and renderProjection as they descend an expression tree. Two more ast groups follow the same tree shape through the visitor methods each node type implements: 21 symbols across .rewrite() and 15 across .fold(). query-ast repeats all of it on the MongoDB side (14 symbols of node types, 10 of .rewrite()), and a 12-symbol syntax group covers the PSL red tree, where SyntaxNode.children, childAt, climbingNext, and climbingPrev re-enter each other while walking. Every one of these is a tree walked by functions that call themselves on child nodes, concentrated in the four places this codebase models trees.

See your own codebase mapped like this. Free for 7 days, no credit card.

Start free trial →

Auto-generated by Symvanta from the public repo prisma/prisma at commit dd6c12b , licensed Apache-2.0 .

Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).

Get this for your codebase →