Skip to main content

How to Self-Host Grist: Spreadsheet-Database Airtable Alternative 2026

·OSSAlt Team
gristairtablespreadsheetdatabaseself-hostingdocker2026

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)

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

Comments