What Is Fauna? The Serverless Document Database for Modern Apps

When AI generates a backend and picks a database you've never heard of, Fauna is often the culprit. Here's what it actually is, why AI likes it, and whether you should keep it or swap it out.

TL;DR

Fauna is a serverless document database — you store JSON-like objects, query them with FQL or GraphQL, and never touch a server. It has built-in ACID transactions, a native GraphQL API, and global replication out of the box. Pay only for what you use. Best for transactional apps with variable traffic where you want zero infrastructure. Not ideal for heavy analytics, complex relational joins, or apps with consistent high throughput that would be cheaper on flat-rate Postgres.

Why AI Coders Need to Know About Fauna

You give an AI tool a prompt like "build me a SaaS app with user accounts, teams, and billing." It generates a full backend. You deploy it. Everything works. Then you look at the database config and see something called Fauna.

What is that? Why did AI choose it? Is it safe to use in production?

AI assistants reach for Fauna because it solves a specific, very real problem for solo developers and small teams: you shouldn't have to manage a database server to ship a product. Fauna gives you:

  • No connection pooling headaches (the thing that kills serverless apps on connection strings to traditional databases)
  • No cluster sizing decisions — it scales automatically
  • No read replica configuration, no failover setup
  • A built-in GraphQL API so you can query your data from the frontend immediately
  • ACID transactions that span multiple documents — something many NoSQL databases can't do

That combination is genuinely compelling for builders who want to focus on product, not infrastructure. But Fauna has quirks that bite you if you don't understand what it is. This guide gives you everything you need to use it confidently — or make an informed decision to switch to something else.

If you're new to databases generally, start with what is a database and then come back. If you're deciding between document and relational databases, read NoSQL vs SQL first — it'll make Fauna's trade-offs much clearer.

Real Scenario: AI Picks Fauna for Your App

You're building a project management tool. Teams, projects, tasks, comments. You need user authentication, and you're deploying to Vercel or Netlify — a serverless environment where long-lived database connections are a problem.

Your AI Prompt

"Build a project management API with Next.js. I need collections for users, teams, projects, and tasks. Use a database that works well with serverless functions — no connection pool issues. Include creating a project, adding team members, and querying all tasks for a user. Use TypeScript."

This is exactly the scenario where AI reaches for Fauna. Serverless functions can't maintain persistent database connections the way a traditional server would. Fauna's HTTP-based API sidesteps that entirely — every query is a stateless HTTP request, no connection pool required.

Let's see what gets generated and then understand every piece of it.

What AI Generated

Step 1: Install the Fauna Driver

npm install fauna
# or
yarn add fauna

Step 2: Initialize the Client

// lib/fauna.ts
// Single shared client — safe to reuse across serverless function calls

import { Client } from 'fauna';

if (!process.env.FAUNA_SECRET) {
  throw new Error('FAUNA_SECRET environment variable is not set');
}

export const faunaClient = new Client({
  secret: process.env.FAUNA_SECRET,
});

Step 3: Define Collections in Fauna Dashboard (or via FQL)

// Run these in the Fauna dashboard shell or via a migration script
// Collections are like tables — they hold your documents

// Create collections
Collection.create({ name: "User" })
Collection.create({ name: "Team" })
Collection.create({ name: "Project" })
Collection.create({ name: "Task" })

// Create indexes for querying
// Find projects by team
Project.definition.update({
  indexes: {
    byTeam: {
      terms: [{ field: "teamId" }]
    }
  }
})

// Find tasks by project and assignee
Task.definition.update({
  indexes: {
    byProject: {
      terms: [{ field: "projectId" }]
    },
    byAssignee: {
      terms: [{ field: "assigneeId" }]
    }
  }
})

Step 4: Create a Project (API Route)

// app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { faunaClient } from '@/lib/fauna';
import { fql } from 'fauna';

export async function POST(req: NextRequest) {
  const { name, teamId, ownerId } = await req.json();

  try {
    // fql`` is a tagged template literal — Fauna's query builder
    const result = await faunaClient.query(fql`
      Project.create({
        name: ${name},
        teamId: ${teamId},
        ownerId: ${ownerId},
        createdAt: Time.now(),
        status: "active"
      })
    `);

    return NextResponse.json({ project: result.data });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create project' },
      { status: 500 }
    );
  }
}

Step 5: Query Tasks for a User (with a Transaction)

// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { faunaClient } from '@/lib/fauna';
import { fql } from 'fauna';

export async function GET(req: NextRequest) {
  const userId = req.nextUrl.searchParams.get('userId');

  try {
    // ACID transaction: reads user + their tasks atomically
    // Both reads see the same consistent snapshot of the database
    const result = await faunaClient.query(fql`
      let user = User.byId(${userId})!
      let tasks = Task.byAssignee(${userId}).toArray()

      {
        user: user { id, name, email },
        tasks: tasks {
          id,
          title,
          status,
          dueDate,
          projectId
        },
        taskCount: tasks.length
      }
    `);

    return NextResponse.json(result.data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch tasks' },
      { status: 500 }
    );
  }
}

Step 6: Environment Setup

# .env.local
FAUNA_SECRET=fn_your_secret_key_here

# Get this from Fauna Dashboard:
# 1. Create a free account at fauna.com
# 2. Create a database
# 3. Go to Security → Keys → New Key
# 4. Choose "Server" role for backend use
# 5. Copy the secret — you won't see it again

Understanding Each Part

Documents and Collections

Fauna is a document database — similar to MongoDB in structure. Instead of rows in tables, you store documents, which are JSON-like objects that can have any shape:

// A "Task" document in Fauna looks like this:
{
  id: "392847593847",       // Fauna generates this automatically
  coll: Task,               // Which collection it belongs to
  ts: Time("2026-03-29T10:00:00Z"),  // Last modified timestamp (managed by Fauna)
  title: "Design landing page",
  status: "in_progress",
  dueDate: Time("2026-04-15T00:00:00Z"),
  projectId: "281937465839",
  assigneeId: "109283746529",
  tags: ["design", "frontend"],
  metadata: {
    estimatedHours: 8,
    priority: "high"
  }
}

Each document lives in a collection (Fauna's equivalent of a table). Collections don't enforce a schema by default — you can store different shaped documents in the same collection, which is great for prototyping and terrible for discipline.

Documents are identified by an auto-generated id field. Unlike SQL databases where you'd set up a SERIAL PRIMARY KEY, Fauna handles this automatically. The ts (timestamp) field is also managed by Fauna — it tracks when the document was last modified and is used internally for optimistic locking.

FQL: Fauna Query Language

FQL is Fauna's native query language, and it's unlike anything you've probably seen before. It's not SQL. It's not a JSON query language like MongoDB's. It's a functional expression language where queries are built by composing functions:

// SQL equivalent of SELECT * FROM tasks WHERE project_id = '123' LIMIT 10
Task.byProject("281937465839").paginate(10)

// SQL equivalent of UPDATE tasks SET status = 'done' WHERE id = '392847593847'
Task.byId("392847593847")!.update({ status: "done" })

// Multi-step logic in a single transaction
let project = Project.byId(${projectId})!
let tasks = Task.byProject(project.id).toArray()
let completedCount = tasks.filter(t => t.status == "done").length

project.update({
  completedTasks: completedCount,
  progress: completedCount / tasks.length
})

The ! after byId() is important — it asserts the document exists and throws an error if it doesn't. Without it, Fauna returns null instead of erroring, which can lead to silent failures.

FQL is more powerful than it looks. You can write multi-step logic, call user-defined functions, enforce business rules, and run everything as a single atomic transaction. Think of it less like a query language and more like a scripting language that happens to run inside your database.

Fauna's Native GraphQL API

If FQL feels like too much to learn, Fauna offers a built-in GraphQL API — you upload a schema, and Fauna auto-generates the resolvers, queries, and mutations for you:

# Upload this schema to Fauna → GraphQL → Import Schema
# Fauna generates all CRUD operations automatically

type Task {
  title: String!
  status: String!
  dueDate: Time
  project: Project!
  assignee: User!
}

type Project {
  name: String!
  status: String!
  team: Team!
  tasks: [Task!] @relation
}

type Query {
  findTasksByProject(projectId: ID!): [Task]
  allProjects: [Project!]!
}

After uploading that schema, you can immediately run GraphQL queries against https://graphql.fauna.com/graphql without writing a single resolver. For CRUD-heavy apps where AI is generating a lot of boilerplate, this is a massive time saver.

The trade-off: the auto-generated GraphQL is great for basic operations but hits a ceiling with complex business logic. That's when you drop back down to FQL.

ACID Transactions: The Big Deal

Most document databases (including older versions of MongoDB) struggled with ACID transactions across multiple documents. Fauna was built with multi-document transactions as a first-class feature.

What that means in practice:

// This entire block runs as a single ACID transaction
// If the payment creation fails, the order creation is rolled back automatically
// No partial state. No orphaned records.

let order = Order.create({
  userId: ${userId},
  items: ${items},
  total: ${total},
  status: "pending"
})

let payment = Payment.create({
  orderId: order.id,
  amount: order.total,
  method: ${paymentMethod},
  status: "processing"
})

// Update the user's order count atomically
let user = User.byId(${userId})!
user.update({ orderCount: user.orderCount + 1 })

// Return both created documents
{ order: order, payment: payment }

If any step in that FQL block throws an error, the entire transaction is rolled back. The order isn't created, the payment isn't created, the user's order count isn't incremented. That's ACID. For financial operations, e-commerce, or any workflow where partial writes would corrupt your data, this matters enormously.

Serverless Pricing Model

Fauna doesn't charge you for a server running 24/7. It charges for what you actually use:

Billable UnitWhat CountsNotes
Read Operations Documents read per query Reading 100 docs = 100 read ops
Write Operations Documents created or updated Each create/update = 1 write op
Compute (TCUs) Query complexity + execution time Complex FQL costs more TCUs
Storage GB of data stored Includes indexes and history

The free tier is generous for development: 100K read ops/day, 50K write ops/day, 500K TCUs/day, and 100MB storage. For a real app with moderate traffic, budget $25–$75/month.

The pricing model is great when traffic is variable — nights and weekends cost almost nothing. It can get expensive under sustained high read volume. If you're reading millions of documents per day from a high-traffic feature, compare the cost against a flat-rate managed Postgres instance before committing.

Regions and Global Distribution

Fauna is globally distributed by default. When you create a database, you choose a region group:

Region GroupLocationsBest For
US US East + US West US-focused apps
EU EU West + EU Central GDPR compliance, EU users
Global US + EU + Asia Pacific International apps
Preview Single region Development/testing only

Within a region group, Fauna automatically replicates data across multiple availability zones. You don't configure this — it just works. For a solo developer shipping a global product, getting multi-region replication without any ops work is a real advantage.

The connection string for Fauna is just a secret key — there's no hostname to configure, no port, no SSL settings. The Fauna client automatically routes to the right region based on the key.

What AI Gets Wrong About Fauna

1. Using the Old FQL v4 Syntax

Fauna made a major breaking change between FQL v4 (the old API) and FQL v10 (current). AI trained on older data frequently generates FQL v4 code, which looks completely different and won't work with the current driver:

// ❌ FQL v4 (old — AI generates this from older training data):
import { query as q } from 'faunadb';

const result = await client.query(
  q.Create(
    q.Collection('Task'),
    { data: { title: 'Design landing page', status: 'todo' } }
  )
);

// ✅ FQL v10 (current — what you actually want):
import { fql } from 'fauna';

const result = await faunaClient.query(fql`
  Task.create({
    title: "Design landing page",
    status: "todo"
  })
`);

Fix: If AI generates code using faunadb (the old npm package) or the q.Create / q.Collection syntax, it's using FQL v4. Tell it: "Use FQL v10 with the fauna npm package, not faunadb." The new API is significantly cleaner.

2. Missing the ! Assertion Operator

When fetching a document by ID, AI often forgets the ! that asserts the document exists:

// ❌ Returns null if the task doesn't exist — silent failure:
const task = await faunaClient.query(fql`
  Task.byId(${taskId})
`);
// task.data is null — but your code might try to access task.data.title anyway

// ✅ Throws a DocumentNotFound error if missing — fail loudly:
const task = await faunaClient.query(fql`
  Task.byId(${taskId})!
`);

Returning null instead of throwing an error means your API silently returns incomplete data. In a UI, this shows up as blank fields or crashes when you try to access properties on null.

3. Querying Without Indexes

In FQL v10, you can't run arbitrary field queries without defining an index first. AI sometimes generates code that tries to filter on a field without realizing that field needs an index:

// ❌ This will throw an error — "status" is not indexed:
Task.all().where(.status == "todo")

// ✅ Define the index in the collection first:
// Task.definition.update({ indexes: { byStatus: { terms: [{ field: "status" }] } } })

// Then query using the named index:
Task.byStatus("todo")

This is actually a good constraint — it prevents accidentally running slow full-collection scans — but it surprises developers coming from MongoDB where you can filter by any field at any time.

4. Storing Secrets in Code

AI generates the client initialization and sometimes inlines the secret directly in the code instead of reading it from an environment variable:

// ❌ Never do this — your key will be in git history:
const client = new Client({ secret: 'fn_abc123yourrealkey' });

// ✅ Use environment variables:
const client = new Client({ secret: process.env.FAUNA_SECRET });

Fauna secrets have the same access level as your database. Leaking one means someone can read, write, and delete all your data. See our guide on connection strings and database credentials for best practices.

5. Using Fauna for the Wrong Use Case

The biggest mistake isn't in the code — it's choosing Fauna for a use case it's not suited for. AI doesn't always know to push back on this. Watch out for:

  • Heavy analytics queries — Fauna is optimized for transactional workloads. Aggregating millions of records is slow and expensive. Use a dedicated analytics database or data warehouse for reporting.
  • Complex multi-collection joins — Fauna can reference documents across collections, but it's not a relational database. Deeply joined queries get awkward. If your data model is fundamentally relational, Postgres is a better fit.
  • Sustained high read volume — If you're reading millions of documents daily at consistent load, the per-read pricing may be significantly more expensive than a flat-rate managed database.

How to Debug Fauna Issues

Problem: "unauthorized" or "permission denied" Errors

Debug Prompt

"My Fauna query throws 'unauthorized' even though I have a FAUNA_SECRET set. Here's my client setup: [paste]. Here's the query: [paste]. The key was generated in the Fauna dashboard. What could be wrong?"

Common causes:

  • Wrong key role — "Client" role keys can't write data from a server; use "Server" role for API routes
  • Key was created for a different database — check which database the key belongs to in the dashboard
  • Environment variable not loaded — console.log(process.env.FAUNA_SECRET) to confirm it's defined at runtime
  • Key was deleted or regenerated in the dashboard after you last deployed

Problem: FQL Syntax Errors (Especially from Old AI Output)

// Error: "Cannot find module 'faunadb'" or "q is not defined"
// → You have FQL v4 code. Switch to the fauna package:

// Remove old package:
npm uninstall faunadb

// Install new package:
npm install fauna

// Update imports from:
import Fauna from 'faunadb';
const { query: q } = Fauna;
// To:
import { Client, fql } from 'fauna';

Problem: Index Not Found

Debug Prompt

"My Fauna query throws 'invalid_ref: Collection Task has no index named byProject'. I'm trying to query tasks by project ID. Here's my FQL: [paste]. Show me how to define the missing index and fix the query."

Run this in the Fauna dashboard shell to check what indexes exist and add missing ones:

// Check existing indexes on the Task collection:
Task.definition

// Add the missing index:
Task.definition.update({
  indexes: {
    byProject: {
      terms: [{ field: "projectId" }]
    }
  }
})

// Confirm it was created:
Task.definition.indexes

Problem: Pagination — Getting Only the First Page

Fauna returns paginated results by default, capped at 16 documents. AI often generates code that only processes the first page:

// ❌ Only returns up to 16 tasks:
const result = await faunaClient.query(fql`
  Task.byProject(${projectId})
`);
// result.data.data is an array of up to 16 items
// result.data.after contains a cursor if there are more

// ✅ Get all tasks (use carefully — can be slow for large collections):
const result = await faunaClient.query(fql`
  Task.byProject(${projectId}).toArray()
`);

// ✅ Or paginate explicitly with a page size:
const result = await faunaClient.query(fql`
  Task.byProject(${projectId}).paginate(50)
`);

Fauna vs. the Alternatives

DatabaseBest ForServerless-Friendly?Free Tier
Fauna Transactional apps, global distribution, zero ops Yes — HTTP API, no connection pool Yes — generous for dev
Supabase (Postgres) Relational data, SQL familiarity, full-stack apps Partial — needs connection pooling (PgBouncer) Yes — 2 free projects
MongoDB Atlas Flexible documents, large ecosystem, mature tooling Yes — Atlas Data API available Yes — 512MB free cluster
PlanetScale (MySQL) Relational data, non-blocking schema changes Yes — HTTP connector available Limited free tier
Firebase Firestore Real-time apps, mobile, Google ecosystem Yes — designed for it Yes — Spark plan
DynamoDB AWS-native, massive scale, key-value patterns Yes — native AWS serverless Yes — AWS free tier

Fauna vs. MongoDB: The Detailed Take

Both are document databases storing JSON-like data. The key differences:

  • Transactions: Fauna has always had multi-document ACID transactions. MongoDB added them in v4.0 but they require replica sets and add complexity.
  • Operations model: Fauna is serverless-only with per-operation pricing. MongoDB Atlas offers both serverless and cluster-based pricing.
  • Query language: FQL is more expressive for complex logic but has a steeper learning curve. MongoDB's query language is more familiar and has a massive ecosystem of tools, drivers, and ORMs like Mongoose.
  • Schema flexibility: Both are schemaless by default. MongoDB has more mature schema validation tools.

Fauna vs. Supabase: Which Serverless Option?

This is the comparison most vibe coders actually face. Read the full breakdown in our Supabase vs Firebase comparison, but the short version:

  • Choose Fauna if you want a true document model, built-in multi-document ACID transactions, and a native GraphQL API without schema setup overhead.
  • Choose Supabase if you prefer SQL, want a relational data model, need the broader PostgreSQL ecosystem, or want built-in auth and storage alongside your database.

Both work well with serverless, but Supabase with Postgres still benefits from connection pooling via their built-in PgBouncer. Fauna's HTTP API makes this a non-issue entirely.

What to Learn Next

Frequently Asked Questions

What is Fauna in simple terms?

Fauna is a serverless document database — meaning you store JSON-like objects and you never provision or manage servers. You pay only for what you use (reads, writes, and compute), and Fauna handles replication, scaling, and availability automatically. It also has a built-in GraphQL API and its own query language called FQL, which lets you write complex queries and enforce business logic at the database layer. Think of it as MongoDB's transactional cousin that runs entirely without infrastructure work on your part.

Is Fauna better than MongoDB?

It depends on your priorities. Fauna wins on zero infrastructure management, built-in ACID transactions across multiple documents, and a native GraphQL API. MongoDB wins on ecosystem maturity, community size, ORM support, and flexibility at scale. For vibe coders who want to ship fast without managing a database cluster, Fauna's serverless model is often simpler. For teams with complex data needs or who already know MongoDB, Atlas (MongoDB's managed cloud service) may be the better fit.

What is FQL and do I need to learn it?

FQL (Fauna Query Language) is Fauna's native query language. Unlike SQL, FQL is a functional expression language — queries are built by composing functions rather than writing declarative statements. You don't need to learn it upfront if you use Fauna's built-in GraphQL API, which handles most CRUD operations automatically. FQL becomes important when you need complex logic: multi-step transactions, custom access rules, or queries the GraphQL schema can't express cleanly. If AI generates FQL for you, make sure it's FQL v10 syntax — not the old FQL v4, which looks completely different and uses the deprecated faunadb npm package.

How does Fauna's serverless pricing work?

Fauna charges for read operations, write operations, compute (measured in Transactional Compute Units or TCUs), and storage. There's a generous free tier for development and small projects. Unlike a VM or managed cluster where you pay for uptime whether or not anyone uses your app, Fauna charges only when your code actually runs queries. This makes it cheap for apps with variable or low traffic, but potentially expensive under heavy read-heavy workloads compared to a flat-rate managed database. For most side projects and early-stage products, the free tier covers you entirely. For production apps, budget $25–$75/month depending on traffic patterns.

When should I NOT use Fauna?

Avoid Fauna if you need complex relational joins (it's a document database, not relational), if your app does heavy analytical queries over large datasets (it's optimized for transactional workloads, not analytics), if you need a mature ecosystem with a wide choice of ORMs and tools, or if you have consistent high read traffic where per-operation pricing would exceed a flat-rate Postgres instance. Fauna is best for transactional apps with variable traffic, teams who want zero infrastructure overhead, and use cases where multi-document ACID transactions matter.