Raw capture β raw/grist-core.md
url: https://github.com/gristlabs/grist-core title: Grist author: Grist Labs Inc. date_fetched: 2026-07-29 date_published: 2014
Grist is a modern relational spreadsheet: a hybrid that combines the flexibility of a spreadsheet with the robustness of a database. It's an open-source (Apache 2.0) project by Grist Labs (NYC), with significant contributions from the French government (ANCT DonnΓ©es et Territoires, DINUM). The grist-core repository contains the community edition server.
The project is a large TypeScript + Python codebase: - ~233K lines of TypeScript (app/ directory: server, client, common) - ~45K lines of Python (sandbox/ directory: data engine, formula runtime) - ~490K lines in yarn.lock (dependencies) - Additional C++, shell, Docker, and configuration files
The codebase is structured as:
- app/server/ β Node.js Express server, document management, SQLite storage
- app/client/ β Browser-side SPA: grid views, dashboards, widgets
- app/common/ β Shared code between server and client
- app/gen-server/ β Home database management (Postgres/SQLite)
- app/plugin/ β Plugin API types and interfaces
- sandbox/grist/ β Python data engine: formula evaluation, dependency tracking
- sandbox/grist/functions/ β Built-in spreadsheet functions (802 lines of info functions alone)
- stubs/ β Stub implementations for the community edition
- buildtools/ β Build pipeline, Webpack config, deployment scripts
- test/ β Mocha-based integration and browser tests
Grist can run as a single process or as a horizontally-scaled deployment:
The key insight: Home Servers and Doc Workers run the same code (app/server/lib/FlexServer.ts). Configuration and environment variables determine which role the process plays. This is a clean monorepo-with-dual-deployment pattern.
A Grist document is a .grist file β a SQLite database with a specific schema:
_grist_* tables: managed by the Python data engine. These are metadata tables describing the document structure: _grist_Tables, _grist_Tables_column, _grist_Views, _grist_Views_section, _grist_ACLRules, _grist_Attachments, etc._gristsys_* tables: managed by Node.js. These are system tables: _gristsys_Action (action log), _gristsys_ActionHistory (undo), _gristsys_FileInfo (attachments), _gristsys_PluginData.The SQLite file is the unit of portability β everything is self-contained. Attachments can optionally be stored externally (S3-compatible) to keep .grist files small.
UpdateRecord, AddRecord, RenameTable)AddRecord, UpdateRecord, AddColumn, etc.)Server-side (app/server/lib/):
- FlexServer.ts β Express endpoint setup, initializes all components. The "Flex" means it can be Home Server, Doc Worker, or both.
- ActiveDoc.ts (~2,000+ lines) β The central dispatcher for an open document. Connects NSandbox, DocStorage, GranularAccess, DocClients. Shuttles user actions and doc actions between them. Methods include applyUserActions, fetchTable, applyProposal, getFormulaTimingInfo.
- GranularAccess.ts β Granular access control. Checks user actions before AND after the data engine processes them. Filters what each client sees based on permissions. Handles the complex task of access rules expressed as formulas.
- NSandbox.ts β Manages the Python subprocess. Spawns a child process with pipes for RPC-like communication. Supports multiple sandbox flavors (Docker, gVisor, macOS sandbox, unsandboxed).
- DocStorage.ts β SQLite operations. Translates every Doc Action into SQL. Manages the _gristsys_* system tables. Handles action logging, attachments, and snapshots.
- HostedStorageManager.ts β S3 sync. Fetches .grist files from S3 when docs are opened, syncs back on changes, creates periodic snapshots.
Home DB (app/gen-server/lib/homedb/):
- HomeDBManager.ts β ORM-style manager for the HomeDB (Postgres or SQLite). Handles users, orgs, workspaces, documents, sharing, billing. Entity definitions in app/gen-server/entity/.
Common (app/common/):
- TableData.ts β In-memory table data with Doc Action application logic
- DocData.ts β Set of TableData objects (all data for a document)
- gutil.ts β Utility functions
- DocActions.ts β Type definitions for all Doc Actions and User Actions
- ActionBundle.ts β Action packaging with envelope-based encryption for multi-instance sync
sandbox/grist/)engine.py (~2,000+ lines) β The core Engine class. Owns the dependency graph, recompute map, and the update loop. Manages tables, schema, formula evaluation order via cooperative exception-based scheduling.gencode.py β Dynamic Python code generator. Converts the document schema (tables + columns + formulas) into a Python module at runtime. Each table becomes a class; each formula column becomes a method. Maintains a formula cache to avoid recompilation.depend.py β Dependency graph implementation. Nodes represent columns (table_id, col_id). Edges represent dependencies with Relations for row mapping. Inspired by the Ninja build system's graph.relation.py β Relations map rows between tables. IdentityRelation for same-table, ReferenceRelation for foreign keys, ComposedRelation for chains. Relations determine which dependent rows need recalculation when source rows change.useractions.py β Implementation of all User Actions. Complex actions (like CreateViewSection) are implemented here because Python makes it easier and enables single-step undo.docmodel.py β Convenience interface to document metadata.schema.py β Schema definitions for metadata tables.acl.py β Access control rule parsing and evaluation within the data engine.The schema (ordered dict of tables, each with ordered dict of columns) is compiled into a Python module string and executed via exec(). This is the most distinctive architectural choice in Grist.
For each table, it generates a class like:
class Students:
def Name(rec, table):
return rec.school.Name # a Reference lookup formula
def Grade(rec, table):
return "A" # a literal formula
Formula columns become methods. Non-formula columns become empty slots. Reference column types (e.g., Ref:Schools) generate typed accessor properties. Summary tables get special group-by column handling.
The generated code is cached per formula text. When a formula changes, only that column's method is regenerated β not the entire module. The GenCode class maintains a _formula_cache dictionary keyed by (table_id, col_id, formula_text).
This is the most clever implementation detail. Instead of topologically sorting the dependency graph and evaluating in order (which requires knowing all dependencies upfront), Grist uses a dynamic, exception-driven approach:
engine.recompute_map_update_loop() which iterates through dirty nodesOrderError is thrownThis is essentially cooperative scheduling for formula evaluation. It handles dynamic dependencies (where the set of dependencies changes based on data values) naturally, which a static topological sort cannot. It also handles circular references gracefully β when a cell depends on itself, the engine detects the locked cell and raises CircularRefError.
The approach also distinguishes between formula columns and trigger formulas. Trigger formulas use SingleRowsIdentityRelation which refuses to propagate ALL_ROWS changes β they only recalculate for specific row changes, not full-column changes like renames.
Dependencies are more than "column B depends on column A." The Relation class specifies WHICH rows in the dependent column need recalculation when specific rows change in the source column.
IdentityRelation β same table, row N in column B depends on row N in column A (direct field reference)ReferenceRelation β foreign key: row N in table B depends on the referenced row in table AComposedRelation β chain of relations (e.g., rec.school.address.zip composes ReferenceRelation(PersonβSchool) + ReferenceRelation(SchoolβAddress))SingleRowsIdentityRelation β identity but refuses ALL_ROWS propagation, used for trigger formulasThe edge set in the dependency graph tracks which relations have been observed. When a new relation between two nodes is discovered during formula evaluation, it's added to the graph dynamically.
User Actions (high-level, what the user intended) β Doc Actions (low-level, what actually changes)
User Actions are defined in useractions.py with the @useraction decorator. Examples:
- UpdateRecord(table_id, row_id, col_values) β user edits a cell
- AddRecord(table_id, row_id, col_values) β user adds a row
- CreateViewSection(table_id, parent_id, type, ...) β user adds a widget
Doc Actions are the atomic operations:
- Data: AddRecord, BulkAddRecord, RemoveRecord, UpdateRecord, BulkUpdateRecord
- Schema: AddColumn, RemoveColumn, RenameColumn, ModifyColumn, AddTable, RemoveTable, RenameTable
The Python engine translates User Actions into Doc Actions, evaluating formulas along the way. The Node server translates Doc Actions into SQL. Both Python and Node know about Doc Actions.
This is like a compiler pipeline: User Actions are the source language, Doc Actions are the IR. The separation means: - Undo is straightforward (inverse Doc Actions) - The access control layer can filter Doc Actions before clients see them - Different clients can receive different subsets of Doc Actions based on their permissions
The .grist file is a valid SQLite database. Any SQLite tool can read it. The schema is:
_gristsys_* tables_grist_* tables)These two versions enable migrations. The DocStorage.docStorageSchema has a create() method and a migrations array that are applied sequentially.
The SQLite file stores everything β attachments as blobs in _gristsys_Files, action history in _gristsys_Action and _gristsys_ActionHistory, undo information in _gristsys_Action_step. Attachments can optionally be stored externally (S3) via HostedStorageManager.
For rendering tables with 10K+ rows, the client uses koDomScrolly.js. Instead of creating DOM nodes for every row (which would bring the browser to its knees), it:
- Renders only the visible rows
- Reuses DOM nodes β as the user scrolls, the same <div> elements are updated with new row data and repositioned
- Creates BaseRowModel observables only for visible rows
- Uses the scroll position to compute which rows should be visible and maps DOM elements to those positions
Access rules are formulas that evaluate to boolean. They can reference:
- Cell values in any table
- User attributes (email, name, role)
- Special user object for attribute-based conditions
The GranularAccess.ts module checks actions twice:
1. Before the data engine processes them: "Is this user allowed to make this change?"
2. After the data engine produces Doc Actions: "Should this client see the resulting changes?"
This two-pass approach handles the case where a user modifies a cell they can see, which triggers a formula recalculation in a column they can't see. The intermediate results are filtered before broadcast.
The Python data engine runs in a separate process for isolation (user formulas are arbitrary Python code). Communication is via pipes with a custom RPC protocol. The NSandbox.ts module supports multiple sandbox flavors:
- Docker: Python in a Docker container
- gVisor: Google's application kernel for stronger isolation
- macOS sandbox: Native macOS sandboxing
- Pyodide/Deno: WASM-based Python for environments where separate processes aren't available (Windows, or when Docker isn't practical)
- Unsandboxed: Direct subprocess (for development or trusted environments)
Actions are packaged into envelopes for multi-instance synchronization. Each envelope contains encrypted content addressed to a specific set of recipients. This enables: - Multi-master document collaboration (not fully implemented in the open-source edition) - Encrypted action sharing between instances - Fine-grained access control at the action level
The ActionBundle type includes envelopes (sets of recipients), stored (persistent Doc Actions), calc (computed Doc Actions), and info (metadata). The EncActionBundle extends this with encryption.
app/client/ui, app/client/components, and app/client/ui2018GridView.js is described in the docs as "one of the oldest pieces of code in Grist. And biggest."app/plugin/ with typed APIs for custom widgets, import sources, file parsersvs. Airtable: Grist is open-source and self-hostable. Both are relational spreadsheet-database hybrids. Grist's formula system is Python-based and arguably more powerful (full Python, not a formula language). Airtable has better mobile support and a more polished UI. Grist's access control with formula-based rules is more flexible.
vs. Google Sheets / Excel: These are pure spreadsheets β every cell can have a different type. Grist enforces column types (like a database). Grist has proper relational features (References, Reference Lists, summary tables) that are awkward or impossible in traditional spreadsheets. Grist's Python formulas are more powerful but less accessible than spreadsheet formula languages.
vs. Notion databases: Notion databases are simpler, more document-oriented. Grist is more of a serious data tool with proper spreadsheet-like features (charts, dashboards, complex formulas). Grist handles much larger datasets.
vs. NocoDB / Baserow: These are also open-source Airtable alternatives. Grist distinguishes itself with Python formulas (vs. formula languages), the two-level action system, the dependency graph architecture, and the self-contained SQLite file format.