Skip to main content

Open-source alternatives guide

How to Self-Host Grist: Airtable Alt 2026

Self-host Grist in 2026. Apache 2.0, ~7K stars — Python-formula spreadsheet with relational tables, REST API, custom widgets, and access control. Full Docker.

·OSSAlt Team
Share:

TL;DR

Grist (Apache 2.0, ~7K GitHub stars, Python/TypeScript) is a self-hosted spreadsheet-database hybrid — the closest open source equivalent to Airtable. Airtable charges $20/user/month for business features. Grist gives you relational tables, Python formulas, granular access control (per-column, per-row), REST API, and custom widgets — all in a Docker container. The formula engine runs Python in the browser via Pyodide (no server-side Python required for basic use).

Key Takeaways

  • Grist: Apache 2.0, ~7K stars — spreadsheet + relational database, Python formulas
  • Python formulas: Full Python expression syntax in cells — SUM(), list comprehensions, pandas-like ops
  • Access control: Per-column and per-row permissions — hide specific cells from specific users
  • REST API: Full CRUD on any table, with filter/sort/limit, no extra config needed
  • Custom widgets: Embed custom HTML/JS panels in any document for maps, charts, custom UIs
  • Airtable import: Direct import from Airtable export; preserves tables and basic field types

Grist vs Airtable vs NocoDB

FeatureGristAirtableNocoDB
LicenseApache 2.0ProprietaryAGPL 3.0
GitHub Stars~7K~49K
CostFree$20/user/moFree
Formula languagePythonAirtable formulasExcel-like
Relational linksYesYesYes
REST APIYesYesYes
Row permissionsYesNoNo
Column permissionsYesNoNo
Custom widgetsYesYesNo
Offline/localYesNoYes
AutomationLimitedYesYes
GitHub Stars~7K~49K

Part 1: Docker Setup

# docker-compose.yml
services:
  grist:
    image: gristlabs/grist:latest
    container_name: grist
    restart: unless-stopped
    ports:
      - "8484:8484"
    volumes:
      - grist_persist:/persist
    environment:
      GRIST_SESSION_SECRET: "${GRIST_SESSION_SECRET}"
      APP_HOME_URL: "https://grist.yourdomain.com"
      # Authentication (optional but recommended):
      GRIST_FORWARD_AUTH_HEADER: "X-Forwarded-User"
      # Or use built-in login:
      GRIST_DEFAULT_EMAIL: "${ADMIN_EMAIL}"
      GRIST_SANDBOX_FLAVOR: "gvisor"   # Sandbox for Python formulas
      # Limits:
      GRIST_MAX_UPLOAD_ATTACHMENT_MB: 50
      GRIST_MAX_UPLOAD_IMPORT_MB: 100

volumes:
  grist_persist:
# .env
GRIST_SESSION_SECRET=$(openssl rand -hex 32)
ADMIN_EMAIL=admin@yourdomain.com

docker compose up -d

Visit http://your-server:8484 — Grist opens directly to the workspace.

Part 2: HTTPS with Caddy

grist.yourdomain.com {
    reverse_proxy localhost:8484
}

Part 3: Authentication Options

Option A: Built-in accounts (simplest)

Grist supports email/password accounts out of the box. First user to visit becomes the owner.

environment:
  GRIST_OIDC_IDP_ISSUER: "https://auth.yourdomain.com/application/o/grist/"
  GRIST_OIDC_IDP_CLIENT_ID: "${OIDC_CLIENT_ID}"
  GRIST_OIDC_IDP_CLIENT_SECRET: "${OIDC_CLIENT_SECRET}"
  GRIST_OIDC_SP_HOST: "https://grist.yourdomain.com"

Option C: Forward Auth (Authelia, Caddy basicauth)

environment:
  GRIST_FORWARD_AUTH_HEADER: "X-Forwarded-Email"
  GRIST_IGNORE_SESSION: "true"
grist.yourdomain.com {
    forward_auth localhost:9091 {
        uri /api/verify?rd=https://grist.yourdomain.com
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
    }
    reverse_proxy localhost:8484
}

Part 4: Creating Your First Document

  1. Click + New Document (or + New Empty Document)
  2. A document contains multiple Tables (like Airtable bases)
  3. Add a table: Click + tab → New Table
  4. Add columns: Click + header → choose type (Text, Numeric, Date, Toggle, Reference, etc.)
  5. Add views: Each table can have Grid, Card, and Chart views

Column Types

TypeUse Case
TextNames, descriptions
NumericNumbers, prices
IntegerCounts, IDs
ToggleCheckboxes, booleans
Date / DateTimeDates with optional time
ChoiceSingle-select dropdown
Choice ListMulti-select tags
ReferenceLink to row in another table
Reference ListLink to multiple rows
AttachmentFiles, images
FormulaPython expression

Part 5: Python Formulas

Grist formulas use Python syntax. Every formula column is computed automatically:

# Basic math:
$Price * $Quantity

# String formatting:
$FirstName + " " + $LastName

# Date math:
($EndDate - $StartDate).days

# Conditional:
"Overdue" if $DueDate < TODAY() else "On time"

# Lookup: sum all orders for this customer
SUM(Orders.lookupRecords(Customer=$id).Amount)

# Average of a reference list:
AVERAGE($Items.Price)

# List comprehension:
[item.Name for item in $Tags.lookupRecords()]

# Count related records:
len(Tasks.lookupRecords(Project=$id))

Part 6: Access Control

Grist's access control is the most granular of any spreadsheet tool:

Per-Document Access

In document settings → Access Rules:

# Rule syntax:
# user.Email matches specific address: full access
# user.Role = "viewer": read only
# newRecord.Status != "Private" OR user.Access in ["admin", "manager"]

Per-Table Access

# Example: Users can only see their own rows
user.Email == $Email  # Condition: row is visible if Email matches logged-in user

Per-Column Access

# Hide "Salary" column from non-managers
user.Role in ["manager", "admin"]

Row-Level Permissions

# Column "Owner" contains email address
# Rule: users can only edit rows where they are the owner
user.Email == $Owner

Part 7: REST API

Every Grist document has a built-in REST API — no extra configuration:

# Get your API key:
# Profile → API Key → Create

API_KEY="your-api-key"
DOC_ID="your-document-id"    # From URL: /doc/DOC_ID

# List all tables:
curl "https://grist.yourdomain.com/api/docs/${DOC_ID}/tables" \
  -H "Authorization: Bearer $API_KEY" | jq

# List records from a table:
curl "https://grist.yourdomain.com/api/docs/${DOC_ID}/tables/Tasks/records" \
  -H "Authorization: Bearer $API_KEY" | jq

# Filter records:
curl "https://grist.yourdomain.com/api/docs/${DOC_ID}/tables/Tasks/records?filter=%7B%22Status%22%3A%5B%22In+Progress%22%5D%7D" \
  -H "Authorization: Bearer $API_KEY"

# Add records:
curl -X POST "https://grist.yourdomain.com/api/docs/${DOC_ID}/tables/Tasks/records" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"fields": {"Title": "New task", "Status": "Todo", "Assignee": "alice@example.com"}}]}'

# Update a record:
curl -X PATCH "https://grist.yourdomain.com/api/docs/${DOC_ID}/tables/Tasks/records" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"id": 42, "fields": {"Status": "Done"}}]}'

# Delete a record:
curl -X DELETE "https://grist.yourdomain.com/api/docs/${DOC_ID}/tables/Tasks/records" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"id": 42}]}'

Part 8: Import from Airtable

# 1. Export from Airtable:
# Base → ... → Download CSV (per table)
# Or use Airtable's bulk export:
# Account → Advanced → API → Export workspace as JSON

# 2. Import to Grist:
# New Document → Import from file → CSV / Excel / JSON
# Grist auto-detects column types on import

# 3. For full Airtable base export (.zip with all tables):
# Grist → New Document → ... → Import → Airtable

# Note: Formulas don't migrate — you'll need to re-create them in Python syntax

Part 9: Custom Widgets

Embed HTML/JS panels in any document:

  1. Click + in a section → Custom Widget
  2. Enter the widget URL (or write inline HTML)

Built-in widgets: Map (OpenStreetMap), Markdown viewer, Form, Calendar, Chart

// Custom widget: display a Gantt chart
// Widget receives selected table records via grist.ready() API
grist.ready({requiredAccess: "read table"});
grist.onRecords(function(records) {
  // Build Gantt from records with StartDate/EndDate columns
  renderGantt(records);
});

Maintenance

# Update Grist:
docker compose pull
docker compose up -d

# Backup:
tar -czf grist-backup-$(date +%Y%m%d).tar.gz \
  $(docker volume inspect grist_grist_persist --format '{{.Mountpoint}}')

# Logs:
docker compose logs -f grist

# Export a specific document as SQLite:
# Document → ... → Export → Grist file (.grist = SQLite)

Why Self-Host Grist

Airtable's Pro plan costs $20/user/month. A team of 5 pays $1,200/year for features that include linked records, automations, and field-level permissions. Airtable's Business plan, which unlocks per-field access controls and advanced permissions, is $45/user/month — $2,700/year for a team of five. Grist gives you per-row and per-column permissions out of the box, Python formulas instead of Airtable's proprietary formula language, and a full REST API, all for the cost of a VPS.

The formula language difference is significant for power users. Airtable formulas are a custom dialect that feels like a limited version of Excel. Grist formulas are real Python — you can write list comprehensions, call standard library functions, look up related records with .lookupRecords(), and use pandas-style operations on columns. If you know Python, you already know Grist formulas. This also means you can bring domain-specific logic directly into your spreadsheet without translating between formula dialects.

Access control is Grist's most underrated feature. Airtable limits field-level permissions to its Business tier, and even then it's coarse-grained. Grist lets you write access rules like Python conditions: user.Email == $Owner hides a row from anyone who isn't the row's designated owner. You can hide specific columns based on user role, deny access to records with a specific status field, and build complex hierarchical permission models — all in a self-hosted instance. This makes Grist viable for use cases where some users should only see their own records in a shared database — sales CRMs, project trackers, client portals.

Another advantage of self-hosting is offline and local-first operation. Grist documents are SQLite files. You can open them locally with any SQLite viewer, export them at any time, and restore from backup trivially. There's no vendor lock-in to a proprietary file format.

When NOT to self-host Grist. Grist's automation capabilities are more limited than Airtable's. If your team relies heavily on Airtable's automations (multi-step triggers that run on record changes), Grist doesn't have a direct equivalent. For teams that want a visual no-code automation layer built in, NocoDB combined with an automation tool like Windmill may be a better fit. Also, Grist's UI has a steeper learning curve than Airtable — plan for a short onboarding period for non-technical users.

Prerequisites

Grist runs as a single Docker container with no external database dependency for basic use — it stores documents as SQLite files in the /persist volume. For teams or production deployments, an external PostgreSQL database for the home server metadata is recommended, but a single-user setup works fine with just the default SQLite persistence. Each Grist document is a self-contained SQLite file, which makes export and backup extremely straightforward.

A Hetzner CX22 (2 vCPU, 4GB RAM, 40GB SSD) at €4.50/month is sufficient for a team of 5–10. Grist's Python formula engine (Pyodide) runs in the browser for most operations, so server CPU is mainly used for API calls, formula evaluation on large datasets, and OIDC authentication. Check the VPS comparison guide for alternatives including Hetzner, Linode, and Vultr.

Docker Engine 24+ and Docker Compose v2 are needed. The GRIST_SANDBOX_FLAVOR: "gvisor" setting enables a secure sandbox for Python formula execution — gVisor must be installed on the host for this to work. If gVisor isn't available, set GRIST_SANDBOX_FLAVOR: "unsandboxed" for development, but use gVisor in production since Python formulas can execute arbitrary code. The sandbox prevents a malicious formula from reading files off the server or making network requests.

Storage planning: Grist documents are SQLite files that grow as data is added. A typical team document with thousands of rows and several tables stays under 50MB. Large documents with attachment files (which are stored in the /persist/attachments directory) can grow substantially. The 40GB SSD on a Hetzner CX22 is generous for most use cases, but monitor disk usage if you allow large attachment uploads.

DNS: create an A record for grist.yourdomain.com pointing to your server. Port 80 and 443 must be open for Caddy's ACME certificate provisioning. Let's Encrypt certificates renew automatically every 60 days — ensure your firewall rules remain in place so renewal challenges can complete without manual intervention.

Production Security Hardening

Grist documents can contain sensitive business data — CRM records, financial models, HR information. Securing the instance is important.

UFW firewall. Block all ports except SSH, HTTP, and HTTPS:

ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Grist's internal port 8484 should not be publicly accessible.

Fail2ban. Protect against login brute-forcing:

apt install fail2ban -y

Add a basic jail in /etc/fail2ban/jail.local:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true

Secrets management. The GRIST_SESSION_SECRET is the most critical secret — it signs all session tokens. Generate it with openssl rand -hex 32 and store it in .env with restricted permissions:

chmod 600 .env

Never commit .env to version control.

gVisor sandbox. Enable the gVisor sandbox for Python formula execution in production. Install gVisor on the host:

curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc

Then set GRIST_SANDBOX_FLAVOR: "gvisor" in your compose environment.

SSH hardening. Use key-based authentication only:

# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no

Automatic updates and off-server document backups are essential. Grist stores every document as a SQLite file in the /persist volume. Schedule these backups via Restic — the automated backup guide walks through automated encrypted backups to S3-compatible storage.

For the full security hardening reference, see the self-hosting security checklist.

Troubleshooting Common Issues

"Session secret not set" error on startup. The GRIST_SESSION_SECRET environment variable is required. Ensure it is set in your .env file and that Docker Compose is loading it with env_file: or variable substitution. A missing or empty secret causes Grist to refuse to start.

Python formula returns an error in the cell. Formula errors in Grist show as red cells with an error message. Click the cell to see the full traceback. Common causes: referencing a column name that doesn't exist (case-sensitive), using a Python 3 built-in that Pyodide doesn't support, or a lookupRecords call that returns an unexpected type. Check the column name spelling first.

gVisor sandbox fails to start. If docker compose logs grist shows "sandbox flavor gvisor: not found," gVisor (runsc) is not installed or not registered as a Docker runtime. Follow the gVisor install steps above and then add it to Docker's daemon config: {"runtimes": {"runsc": {"path": "/usr/local/bin/runsc"}}} in /etc/docker/daemon.json, then restart Docker.

OIDC login fails with "redirect_uri mismatch." The GRIST_OIDC_SP_HOST must exactly match the redirect URI registered in your identity provider. If your Grist URL is https://grist.yourdomain.com, both the env var and the OIDC provider's allowed redirect URIs must use that exact URL, including the trailing slash if required by the provider.

Large document loads slowly. Grist documents are SQLite files and can slow down when tables grow very large (hundreds of thousands of rows). For large datasets, consider splitting data across multiple documents and linking them via references, or using the REST API to paginate data in from an external database rather than storing it directly in Grist.

Backup restore fails with "database is locked." This happens when Grist has the SQLite file open during restore. Stop the Grist container before restoring a backup: docker compose down, restore the volume content, then docker compose up -d.

Custom widget not loading. Custom widgets in Grist load from URLs you provide — they're essentially iframes loading web content. If a custom widget shows a blank panel, check the browser console for Content Security Policy (CSP) violations. Grist allows widgets loaded from any URL, but the widget's own server must allow framing. If you're developing your own widget, add X-Frame-Options: ALLOWALL or appropriate CSP headers to the widget server.

API returns 403 for valid API key. Grist API keys are per-user and associated with specific document access permissions. A valid API key does not grant access to all documents — only documents the key's owner has access to. If your API call gets a 403, verify the API key belongs to a user who has been explicitly granted access to that specific document. Document access is managed in Document Settings → Access Rules, not at the account level.

Formula column shows "TypeError" for some rows. Python formulas in Grist operate on every row individually, and a formula that works for most rows may fail on rows with missing or null values. Use Python's if/else to handle null cases: $Price * $Quantity if $Price else 0. The EMPTY() function is also useful for checking whether a cell is empty before operating on it. Grist shows the formula error in the cell and the full traceback in the formula editor — click the error cell and inspect the editor for the specific Python error.

Documents are slow to open for first-time users. Grist documents with complex Python formulas take a moment to initialize because the formula engine (Pyodide) loads in the browser. This is a one-time cost per session — subsequent formula evaluations are fast. For very large documents (tens of thousands of rows with many formula columns), consider whether some computations can be moved to the REST API layer rather than being recalculated in formulas on every page load.

See all open source spreadsheet and database tools at OSSAlt.com/categories/productivity.

See open source alternatives to Airtable on OSSAlt.

The SaaS-to-Self-Hosted Migration Guide (Free PDF)

Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.

Join 300+ self-hosters. Unsubscribe in one click.