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.
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)
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.