← All posts
·10 min read

Claude Code with SurrealDB: Multi-Model Without Confusion

Claude CodeSurrealDBDatabaseMulti-Model
Claude Code with SurrealDB: multi-model without confusion

Why SurrealDB without CLAUDE.md becomes a slow PostgreSQL clone

SurrealDB is a multi-model database that supports document, graph, and relational patterns in a single SurrealQL query language. The whole point is that you do not pick a model up front. You model the data the way it actually relates, and you query it with the operators that fit the relationship.

Claude Code does not know any of this unless you tell it. Without a CLAUDE.md in place, Claude treats SurrealDB as a SQL database with a slightly different syntax. It writes manual joins instead of using record links. It denormalises into nested arrays instead of using graph relations. It defines schemaless tables when schemafull would catch errors at insert time. It skips the PERMISSIONS clause on tables, which means anyone who can connect can read everything.

This guide gives you the CLAUDE.md configuration that anchors Claude Code to how SurrealDB actually works: record links over joins, graph traversal via the arrow operators, schemafull tables for anything that ships to production, and PERMISSIONS clauses that map to your auth model. If you are comparing multi-model databases, Claude Code with Convex and Claude Code with Firebase cover adjacent shapes.

The SurrealDB CLAUDE.md template

The CLAUDE.md at your project root is read at the start of every Claude Code session. For a SurrealDB project it needs to declare: the schemafull versus schemaless rule, the record link syntax, the graph operators, the permissions structure, the LIVE SELECT pattern, and the migration story.

# SurrealDB rules

## Stack
- SurrealDB 2.x (self-hosted or Cloud)
- surrealdb-js (TypeScript) or surrealdb-rs (Rust) for app code
- SurrealQL for all queries , NEVER fall back to raw SQL-style syntax
- Schemafull tables for production, schemaless only for prototyping

## Table definition rules (STRICT)
- ALWAYS DEFINE TABLE ... SCHEMAFULL for production tables
- ALWAYS DEFINE FIELD for every expected field with TYPE
- ALWAYS include PERMISSIONS clause , default DENY unless explicitly granted
- TYPE record<table_name> for foreign keys , never TYPE string with a manual link
- TYPE option<...> for nullable fields , never TYPE any

## Record link rules
- Foreign keys use record IDs (table:id format), not strings
- Use FETCH to follow links in SELECT , never write a manual subquery
- Use the ->relation->target arrow syntax for graph edges
- Use the <-relation<-source arrow for reverse traversal
- NEVER simulate joins with two queries and JavaScript stitching

## Graph relations
- RELATE statement for creating edges between records
- Edge tables defined with DEFINE TABLE ... SCHEMAFULL like normal tables
- Edge properties go on the edge table, not the source or target
- Traversal uses arrow operators , ->edge->target , <-edge<-source

## Permissions
- ALWAYS define PERMISSIONS at the table level
- Use $auth.id and $auth.role for the standard auth checks
- FOR SELECT, FOR CREATE, FOR UPDATE, FOR DELETE each get a clause
- Default to FOR SELECT NONE on any table not explicitly readable

## LIVE SELECT pattern
- LIVE SELECT is the reactive primitive
- Subscribe in the client , emits diffs on row change
- NEVER poll a regular SELECT to simulate reactivity

## Required statements at project start
- DEFINE NAMESPACE ns
- DEFINE DATABASE db
- DEFINE SCOPE for user-level auth (signup, signin queries)
- DEFINE TOKEN for service-level access

## Migration story
- Schema lives in surreal/schema.surql
- ALWAYS edit schema.surql first, then apply via `surreal import`
- Never run DEFINE statements ad-hoc against production

Drop this into CLAUDE.md, swap in your namespace and database names, and let Claude generate against it. The constraints look strict because the SurrealDB defaults are permissive. Production deployments need the strict end of the spectrum.

Record links replace foreign keys

In SurrealDB a record ID looks like user:abc123. It is not a string. It is a typed reference to a row in the user table. The query language treats it as a navigable pointer, not as opaque text.

Claude defaults to storing references as strings because that is what most relational and document stores do. The CLAUDE.md rule "TYPE record for foreign keys" is the line that prevents this. With it in place, Claude generates field definitions like DEFINE FIELD author ON post TYPE record<user>. Without it, Claude generates DEFINE FIELD author ON post TYPE string and then writes manual joins to follow the reference.

Once the field is a record type, FETCH does the rest. SELECT *, author FROM post FETCH author returns the post with the full author record inlined, with one round-trip and one query plan. There is no JavaScript stitching, no N+1 problem, no separate cache layer. Claude needs the CLAUDE.md to nudge it toward FETCH instead of the manual pattern.

Graph edges and the arrow operators

The arrow operators are SurrealDB's signature feature. ->edge->target walks forward across an edge. <-edge<-source walks backward. Chained arrows traverse multi-hop paths in a single query. The syntax looks unfamiliar but maps directly onto how you actually think about graph relationships.

The standard example is a follow relationship. Two records: user:alice and user:bob. An edge: RELATE user:alice->follows->user:bob. To find Alice's followees: SELECT ->follows->user FROM user:alice. To find Bob's followers: SELECT <-follows<-user FROM user:bob. To find followers of followers: SELECT <-follows<-user<-follows<-user FROM user:bob. The same data, three different shapes, no joins.

Claude does not reach for arrow operators by default. It denormalises followers into an array on the user record, or it writes a follows table with manual ID columns and falls back to subqueries. Both shapes work but neither is idiomatic, and both perform worse than the edge table once the graph is non-trivial. The CLAUDE.md rule above is what flips Claude into the arrow-operator default.

Get Claudify. Skills, hooks, and CLAUDE.md templates that make Claude Code generate SurrealQL that uses the actual multi-model features.

Schemafull versus schemaless, and why production wants schemafull

SurrealDB lets you skip schema. A schemaless table accepts any document shape and stores it as is. This is genuinely useful for prototyping, for ingest layers where the shape is unknown, and for evolving formats. It is dangerous for production because mistakes do not surface at insert time. They surface at read time, in a different service, days later, when the shape is wrong.

The CLAUDE.md template above defaults to schemafull. Every field gets a TYPE. Every required field is non-optional. Type mismatches fail at INSERT, before bad data lands in the table. Claude generates DEFINE TABLE statements with the SCHEMAFULL keyword and the matching DEFINE FIELD statements, instead of leaving the shape open.

The trade-off is verbosity. A schemafull table with twenty fields requires twenty DEFINE FIELD lines. The trade-off is worth it. Bugs that schemafull catches at the boundary are bugs that schemaless tables ship to production silently.

Permissions on every table, no exceptions

SurrealDB tables default to open. Any connection with a valid session can read from any table that does not have a PERMISSIONS clause. This is the opposite of every relational database's default. Claude does not know this default unless you tell it.

The CLAUDE.md rule "ALWAYS include PERMISSIONS clause" plus "default DENY unless explicitly granted" is the security floor. With it in place, Claude generates table definitions like:

DEFINE TABLE post SCHEMAFULL
  PERMISSIONS
    FOR SELECT WHERE published = true OR author = $auth.id,
    FOR CREATE WHERE $auth.id != NONE,
    FOR UPDATE, DELETE WHERE author = $auth.id;

Without it, Claude generates the table with no PERMISSIONS clause and the table is world-readable. The fix is in the template, not in Claude. The template above codifies the right default.

LIVE SELECT is the reactive primitive

LIVE SELECT is SurrealDB's answer to reactive queries. Subscribe with LIVE SELECT * FROM post WHERE author = user:alice and the server pushes diffs whenever a matching row changes. No polling, no websocket plumbing, no manual subscription state.

Claude reaches for polling by default. It writes a setInterval that re-runs a SELECT every few seconds, because that is the pattern it has seen in most stacks. The CLAUDE.md rule "NEVER poll a regular SELECT to simulate reactivity" is what redirects Claude to LIVE SELECT. The client-side handler then receives CREATE, UPDATE, and DELETE notifications with the changed row data, and the UI updates from the diff stream.

Pattern Use when Avoid when
Static SELECT One-shot read, no subscription needed UI needs to refresh on change
LIVE SELECT UI needs to react to data changes Data is static or rarely updated
Polling Source does not support push SurrealDB or any push-capable backend
RELATE then SELECT Graph relationships Tree relationships (use nested records)

Auth via SCOPE and TOKEN

SurrealDB has two auth primitives. SCOPE is for user-level auth with signup and signin queries defined in the scope itself. TOKEN is for service-level access with a signed key. Claude needs to know which one to use for which case.

For user-facing apps, the pattern is: define a user scope with the signup and signin queries embedded. Signup inserts into the user table after hashing the password. Signin verifies the password and returns a JWT. The client uses the JWT for subsequent connections. Permissions clauses then check $auth.id and $auth.role against the row being accessed.

For service-to-service traffic, the pattern is TOKEN. Define a key. Issue tokens signed with the key. Service clients present the token on connection. No password roundtrip.

The CLAUDE.md template marks both as required statements. Claude will generate the SCOPE definition with the right signup/signin shape when prompted, but only if the template flags this as a project-level concern. Otherwise Claude is likely to invent a custom auth layer on top of SurrealDB, which defeats the point of the built-in scope system.

Verifying Claude's SurrealQL before it ships

A CLAUDE.md is necessary but not sufficient. For SurrealDB the verification loop runs against the INFO FOR DB and INFO FOR TABLE introspection statements, which return the live schema as JSON. The check is:

  1. Generate the schema change with Claude
  2. Apply to a scratch database
  3. Run INFO FOR DB to confirm the namespace and tables exist
  4. Run INFO FOR TABLE <name> to confirm the SCHEMAFULL flag, fields, and permissions
  5. Run the test queries and inspect the EXPLAIN output for index usage

A hook that runs these checks automatically after every schema change and feeds the result back to Claude tightens the loop further. Claude can then iterate against actual database state instead of against an assumed schema. For broader hook patterns, Claude Code skills covers how to package the verification loop.

Migrations and the import command

SurrealDB does not have a migration framework in the Rails or Prisma sense. The pattern is: keep your schema in a .surql file, version-control it, and apply with surreal import. Idempotent statements like DEFINE FIELD are safe to re-run. Destructive changes like REMOVE FIELD need a separate migration file.

Claude needs the schema-file-first rule in CLAUDE.md to stop it from running DEFINE statements ad-hoc through the CLI or client. The rule "Schema lives in surreal/schema.surql" and "Never run DEFINE statements ad-hoc against production" is the line that keeps everything traceable in git. For larger projects, a sequence of numbered migration files plus a MIGRATIONS table that tracks applied versions gives you a hand-rolled migration system that fits how SurrealDB actually behaves.

Wrapping up

SurrealDB earns its keep when you actually use the multi-model features. Record links instead of strings. Arrow operators instead of joins. LIVE SELECT instead of polling. Schemafull tables instead of permissive schemaless ones. Permissions clauses on every table instead of relying on app-layer filtering.

Claude Code without a CLAUDE.md falls back to the relational patterns it has seen the most, and the SurrealDB you end up with is a slower, less safe PostgreSQL with unfamiliar syntax. With the template above, Claude Code generates SurrealQL that uses the actual primitives, defaults to safe permissions, and treats the database as the multi-model store it is.

Get Claudify. Skills, hooks, and CLAUDE.md templates that make Claude Code generate SurrealQL that uses the actual multi-model features.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame