So What Actually Happens When Your Database Tries to Be Five Databases
Postgres is the forklift of self-hosted databases. Not exciting, occasionally annoying to configure, but it moves the couch every single time and your neighbors never have to hear about it. SurrealDB showed up promising to also be the couch, the moving truck, and the guy who unpacks boxes on the other end: one binary that claims to do documents, graphs, relational tables, full-text search, and realtime subscriptions.
Here’s my verdict up front. SurrealDB is real software with a real use case: it is production-viable for a specific shape of app, one where your data is naturally document-and-graph shaped and you’d otherwise be bolting Neo4j onto Mongo onto Postgres to get the same result. It is not a general Postgres replacement, and if your app is “users have posts have comments,” picking SurrealDB over Postgres in 2026 is choosing the science project. I ran the same small data model through both to find the actual line, and it’s narrower than the marketing site suggests.
What SurrealDB Is Actually Selling
SurrealDB bills itself as a “multi-model” database: schemaless or schemaful documents, native graph edges between records, relational-style queries through record links, full-text and vector search, and a built-in realtime layer, all through one query language called SurrealQL and one server process. The pitch is that instead of running Postgres for your tables, Redis for your cache, Neo4j for relationships, and a separate pub/sub layer for realtime updates, you run one thing.
That pitch is compelling on paper. Whether it holds up depends entirely on whether you need the graph and realtime pieces, because that’s where SurrealDB earns its keep. If you don’t need them, you’re running an unfamiliar database engine to get features you were never going to use.
Getting It Running: The Easy Part
Full example: Clone the working files at github.com/KingPin/sumguy-examples/self-hosting/surrealdb-vs-postgres
Both databases are a docker compose up away. Here’s SurrealDB 3.2.4, the current stable release as of September 2026:
services: surrealdb: image: surrealdb/surrealdb:v3.2.4 user: root command: start --user root --pass changeme rocksdb:/data/sumguy.db environment: SURREAL_BIND: 0.0.0.0:8000 ports: - "8000:8000" volumes: - ./data:/dataThe rocksdb:/data/sumguy.db path is the on-disk storage engine. SurrealDB also ships surrealkv:// as a native embedded engine and plain memory for throwaway testing, but RocksDB is what you want for anything real. I ran this compose file, hit curl http://localhost:8000/health, and got a 200 back in under three seconds. The user: root line matters: the image runs as a non-root user, and without it the bind-mounted ./data directory is owned by root and SurrealDB exits with a permission error on first boot.
Postgres, for comparison:
services: postgres: image: postgres:18-alpine environment: POSTGRES_PASSWORD: changeme POSTGRES_DB: sumguy ports: - "5432:5432" volumes: - ./pgdata:/var/lib/postgresqlBoth are one file, both boot in seconds. Nobody wins the “who’s easier to start” contest. The difference shows up once you start putting data in.
Modeling Data: Documents, Records, and the Graph Thing
This is where SurrealDB stops looking like “Postgres with a different syntax” and starts looking like something else entirely. A DEFINE TABLE ... SCHEMAFULL block gets you Postgres-style column enforcement on top of a document store:
DEFINE TABLE person SCHEMAFULL;DEFINE FIELD name ON person TYPE string;DEFINE FIELD age ON person TYPE int ASSERT $value >= 0;DEFINE FIELD email ON person TYPE string PERMISSIONS FOR update NONE;
Notice person:alice. Record IDs are first-class in SurrealDB: you pick the ID at creation time instead of waiting for a database-assigned serial or UUID, and every record is addressable as table:id from anywhere in a query. That’s useful for anything where your app already has a natural key (usernames, slugs, external IDs) and doesn’t want a lookup round trip just to find the row.
One gotcha I hit running this: field-level PERMISSIONS only accepts select, create, and update. Try to add delete to a field permission and SurrealDB throws a parse error, Can't define permission DELETE for fields. Delete restrictions belong on the table, not the field. Minor, but it’ll cost you five minutes the first time.
Now the graph part, which is the actual reason to consider SurrealDB. RELATE creates a directed edge between two records, and that edge is itself a full record that can carry fields:
RELATE person:alice->follows->person:bob SET since = time::now();
SELECT ->follows->person AS following FROM person:alice;That last query walks the graph one hop out from Alice and returns everyone she follows, no join table, no recursive CTE. I tested a two-hop version too (SELECT ->follows->person->follows->person AS network FROM person:alice) and it just works, returning the friends-of-friends set directly.
Here’s the same “who does Alice follow” question in Postgres, modeled the conventional way with a join table:
CREATE TABLE person (id serial PRIMARY KEY, name text, age int);CREATE TABLE follows ( follower_id int REFERENCES person(id), followee_id int REFERENCES person(id), since timestamptz DEFAULT now(), PRIMARY KEY (follower_id, followee_id));
SELECT p.nameFROM follows fJOIN person p ON p.id = f.followee_idWHERE f.follower_id = 1;For one hop, that’s not meaningfully worse than SurrealQL. Where Postgres starts hurting is multi-hop traversal: “friends of friends of friends” turns into a recursive CTE with UNION ALL and a depth counter, and it gets slower and uglier with every extra hop because every extra hop is another self-join over the edge table. If your app’s core feature is “who’s connected to whom, how many hops away,” SurrealDB’s native graph edges are doing real work Postgres makes you write by hand. If your app has one belongs_to relationship and a handful of joins, you’ve just traded SQL everyone on your team already knows for a query language nobody’s seen before, to solve a problem you didn’t have.
Record links close the gap for the simple case. A field typed record<table> acts like a foreign key, and dot notation auto-resolves it in a SELECT without an explicit join:
DEFINE TABLE post SCHEMAFULL;DEFINE FIELD title ON post TYPE string;DEFINE FIELD author ON post TYPE record<person>;
CREATE post:p1 SET title = "Hello SurrealDB", author = person:alice;
SELECT title, author.name FROM post;That returns { title: 'Hello SurrealDB', author: { name: 'Alice' } } in one query, no JOIN keyword anywhere. I ran this and it’s the one piece of SurrealQL that felt like a real quality-of-life win over SQL rather than just a different spelling of the same thing.
Realtime: The Feature Postgres Doesn’t Have Natively
LIVE SELECT is SurrealDB’s built-in subscription mechanism. Open a WebSocket connection, run this, and the connection gets pushed every future change to the result set:
LIVE SELECT * FROM person;I tested it over ws://localhost:8000/rpc: the query returns a UUID for the live subscription, then every subsequent CREATE or UPDATE on person gets pushed down the same socket as the full updated record. Switch to LIVE SELECT DIFF FROM person; and you get JSON Patch operations instead. No polling, no separate message broker, no LISTEN/NOTIFY plumbing.
Postgres has LISTEN/NOTIFY, and it works, but it’s a notification channel, not a query subscription. You get told something changed, then you run your own query to find out what. Wiring that into a frontend means either a custom WebSocket relay or reaching for a bolt-on like Postgres logical replication plus a CDC tool such as Debezium, which is a lot of moving parts for “tell my UI when a row changes.” If your app is a live dashboard, a chat feature, or anything with a “someone else is editing this” indicator, SurrealDB’s LIVE SELECT replaces a chunk of infrastructure you’d otherwise assemble yourself. If your app refreshes on page load and that’s fine, this feature buys you nothing.
Full-Text Search: One Fewer Sidecar
Search is the other piece homelabbers usually bolt on separately, Meilisearch or a Postgres tsvector column with a GIN index. SurrealDB has it built in through DEFINE INDEX ... FULLTEXT:
DEFINE ANALYZER simple TOKENIZERS class FILTERS lowercase, ascii;DEFINE INDEX article_body ON post FIELDS title FULLTEXT ANALYZER simple BM25 HIGHLIGHTS;
CREATE post:s1 SET title = "SurrealDB search test", author = person:alice;SELECT * FROM post WHERE title @@ "search";I ran this against 3.2.4 and it returned the matching row using BM25 ranking under the @@ operator, no separate search service, no reindex job to babysit. Note the keyword is FULLTEXT, not SEARCH: that’s one of the naming changes that shipped with 3.0, so any tutorial still showing SEARCH ANALYZER is talking about 2.x and will throw a parse error on current SurrealDB.
Postgres does the equivalent with to_tsvector and a GIN index, which is fewer keystrokes once you already know Postgres and zero keystrokes if your ORM already generates it:
ALTER TABLE post ADD COLUMN search_vec tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;CREATE INDEX post_search_idx ON post USING gin(search_vec);
SELECT * FROM post WHERE search_vec @@ plainto_tsquery('english', 'search');Both work. Postgres full-text search is older, better documented, and has decades of “here’s how to handle typos and stemming” answers on the internet. SurrealDB’s version means one less container in your compose file if you’re already committed to it for the graph and realtime pieces. It is not, on its own, a reason to switch.
The License, Because Someone Will Ask
SurrealDB ships under the Business Source License 1.1, not a permissive open-source license, and GitHub’s own license detector correctly flags it as “Other” rather than matching it to a standard SPDX license. I pulled the LICENSE file straight from the surrealdb/surrealdb repo to check the actual terms rather than trust a badge. The Additional Use Grant says you can’t run the Licensed Work as a “Database Service,” defined as offering it to third parties who create or manage their own schemas and tables through it. Self-hosting SurrealDB to back your own app, for your own users, falls outside that restriction as long as those users are not creating their own schemas or tables. You only hit the restriction if you’re trying to resell managed SurrealDB hosting.
The Change Date is 2030-01-01, at which point the license converts to Apache 2.0 for every 3.x release. That is the BSL model MariaDB created: source-available now, open-source later, timed to stop cloud vendors from repackaging your database as a competing managed service before you’ve built a business around it. For a homelab or a single-company app, this license is a non-issue. It only bites you if your plan was to stand up “Surreal-as-a-Service” for other people.
Maturity: The Part That Actually Matters
SurrealDB is at v3.2.4 as of September 2026 (released August 17, with a v3.1.6 patch following on September 1 and a v3.3.0 beta already cutting), and the project has crossed 33,000 GitHub stars. That’s a real, actively maintained project, not an abandoned experiment. But version 3.0 was a breaking-changes release relative to 2.x, and 2.0 was a breaking-changes release relative to 1.x. This is a database that shipped two breaking major versions in 17 months (2.0 in September 2024, 3.0 in February 2026), and each one needed a data migration step (surreal fix for 1.x, an export and re-import for 2.x). Postgres also needs pg_upgrade or a dump and restore at each major version, but pg_upgrade has reused the existing data files in place since PostgreSQL 9.0 in 2010, and the SQL you wrote a decade ago still runs.
That history is the actual risk, more than any single missing feature. Running SurrealDB in 2026 means accepting that the next major version might change how your existing data reads, and you should plan your upgrade path (test on a snapshot, don’t docker compose pull production on a Friday) accordingly. Postgres extensions, backup tooling, and operational playbooks are two decades deep. SurrealDB’s equivalents are getting there, but “getting there” and “there” are different sentences.
So, Real Database or Science Project
Real database, narrow lane. Reach for SurrealDB when your data model is graph-shaped (social connections, permission trees, recommendation networks) or when built-in realtime subscriptions would replace infrastructure you’d otherwise hand-build, and you’re comfortable being an early adopter on the operational side. Reach for Postgres, still, for anything with a conventional relational shape: users, posts, orders, invoices, the stuff every homelab app is actually made of. Running SurrealDB for a blog’s comment system is hiring a forklift to move one couch cushion. Technically it works. Your 2 AM self will not thank you when the schema shifts under a major version bump and you’re diffing SurrealQL semantics instead of sleeping.
Common Questions
Can SurrealDB replace Postgres for a typical self-hosted app?
No, not for typical CRUD apps with users, posts, and orders. Postgres wins on tooling maturity, SQL familiarity, and backup ecosystem for straightforward relational data. SurrealDB earns its place only when your app needs native graph traversal or built-in realtime subscriptions that would otherwise require bolting Neo4j or a message broker onto Postgres.
Is SurrealDB’s Business Source License a problem for self-hosting?
No. The Additional Use Grant only restricts running SurrealDB as a “Database Service” for third parties who manage their own schemas, meaning reselling managed SurrealDB hosting. Self-hosting it to back your own app, for your own users, is fully permitted today. The license for SurrealDB 3.x converts to Apache 2.0 on 2030-01-01.
Does SurrealDB support ACID transactions like Postgres?
Yes, SurrealDB supports multi-statement ACID transactions via BEGIN TRANSACTION and COMMIT TRANSACTION, and the RocksDB storage engine commits every transaction to disk by default. The SURREAL_DATASTORE_SYNC_DATA setting controls that behavior and defaults to every. The bigger risk is version-to-version schema and storage changes, not transaction safety within a given release.
What storage engine should I use for SurrealDB in production?
Use rocksdb:// for a single-node self-hosted deployment; it is the on-disk engine built for durability and is what the official Docker examples default to. surrealkv:// is a native Rust engine that the SurrealDB docs still label beta and advise against for conservative production servers, and plain memory loses everything on restart, so reserve it for local testing only.
Can I run SurrealDB and Postgres side by side for the same app?
Yes, and it is a reasonable pattern: keep conventional relational data (users, billing, orders) in Postgres and move only the graph-shaped or realtime-heavy piece (social graph, live activity feed) into SurrealDB. That avoids migrating your whole schema to an unfamiliar query language just to get one feature.