How to Self-Host Grist: Spreadsheet-Database Airtable Alternative 2026
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
| Feature | Grist | Airtable | NocoDB |
|---|---|---|---|
| License | Apache 2.0 | Proprietary | AGPL 3.0 |
| GitHub Stars | ~7K | — | ~49K |
| Cost | Free | $20/user/mo | Free |
| Formula language | Python | Airtable formulas | Excel-like |
| Relational links | Yes | Yes | Yes |
| REST API | Yes | Yes | Yes |
| Row permissions | Yes | No | No |
| Column permissions | Yes | No | No |
| Custom widgets | Yes | Yes | No |
| Offline/local | Yes | No | Yes |
| Automation | Limited | Yes | Yes |
| 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.
Option B: Authentik / OIDC (recommended for teams)
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
- Click + New Document (or + New Empty Document)
- A document contains multiple Tables (like Airtable bases)
- Add a table: Click + tab → New Table
- Add columns: Click + header → choose type (Text, Numeric, Date, Toggle, Reference, etc.)
- Add views: Each table can have Grid, Card, and Chart views
Column Types
| Type | Use Case |
|---|---|
| Text | Names, descriptions |
| Numeric | Numbers, prices |
| Integer | Counts, IDs |
| Toggle | Checkboxes, booleans |
| Date / DateTime | Dates with optional time |
| Choice | Single-select dropdown |
| Choice List | Multi-select tags |
| Reference | Link to row in another table |
| Reference List | Link to multiple rows |
| Attachment | Files, images |
| Formula | Python 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:
- Click + in a section → Custom Widget
- 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.