Concepts

Data as files

Why Nimbly keeps data as plain files in Git, what stays out, and how it stays fast without a database.

Nimbly keeps data as plain files in the application repository. There is no database server. A resource is a folder, its definition is a JSON file, and every record is a JSON file of its own. This is a deliberate choice, and it shapes how a Nimbly project is built, shared and run.

ext/data/projects/
  .meta                 the definition: fields, validation, indexes, permissions
  4f1c9a2e7b...         one record
  9d03b6e1c4...         another record

One definition, everything follows

A resource is described once, in its .meta. From that definition Nimbly provides the admin overview and forms, validation, the REST API and its OpenAPI description, permissions per operation, the history of every change, and the context the built-in agent uses. Add a field to the .meta and all of them follow. There are no migrations: a new field is simply empty in older records until someone fills it.

One id per record, everywhere

Every record gets a short random id when it is created, such as 1ztuvo56o5vze6t0. The id is the record's file name, and it never changes.

Databases usually number their rows 1, 2, 3. Those numbers belong to one database: another environment hands out the same numbers to different records, so moving or merging data means renumbering records and fixing every reference to them. In Nimbly that problem does not exist:

  • A record is the same record everywhere: in development, on staging, in production, in Git, in the API and in an export.
  • References hold. A field that points to another record stores its id, and keeps pointing to the same record wherever the data goes.
  • Content moves freely. Records made on different machines or branches merge without clashes, and an import into another site keeps every id.
  • Your own ids work too. A record can take the id it has in another system, such as an order number.

Visitors never see these ids: public addresses use a readable slug, such as /projects/harbour-atlas.

No database layer

Because the data is files, a whole layer of a typical web stack is simply absent:

  • no database server to install, run, secure, tune and back up on its own,
  • no ORM or query layer between the code and the data,
  • no migrations, and no schema to keep in step between development, staging and production,
  • no connection settings or database credentials,
  • no dumps to export and import to share or move a site.

A Nimbly site is a folder: code, content and data together. That keeps the stack small, the hosting simple, and every part of the application readable.

Content lives in Git, next to the code

Because records are files in ext/, content is versioned with the code that uses it:

  • History and rollback. Every change to content is a commit you can inspect, compare and revert, in the same history as the templates that show it.
  • Branches. A feature branch carries its own content: a new resource with sample records, a reworded page, a changed menu. Review and merge content like code.
  • Small, clear diffs. One record is one file, so a change touches only that record, and two people editing different records never conflict.
  • Environments stay in step. Development, staging and production share content through the repository. When editors work on production, its scheduled ext:sync commits their changes and pushes them back, so the next deploy and every developer build on them. See Environments.

Clone, and the site is there

Cloning the application repository brings every record, page text, setting, translation and media file with it. Run setup and the local site shows the real content straight away: no database to install, no dump to import, no connection to configure. The same goes for an AI coding agent: it reads the data definitions and the records as files, next to the code that uses them.

What stays out of Git

Not everything a site holds belongs in a repository. Setup writes an ext/.gitignore that keeps these on each environment:

Kept per environment Why
User accounts Passwords and personal data do not travel with the code; each environment has its own admin
Sessions, job queue, scheduler state Runtime state that changes every minute
Record history Kept 90 days on the environment where the change was made
Caches and resized images Rebuilt on demand
.env Credentials and the PEPPER that encrypted fields depend on

So a repository holds the site's content and structure, while a backup of a production site also covers what Git does not. See Backup and recovery.

Fast without a database

Reading a file is fast, and Nimbly avoids reading what it does not need:

  • Indexes. A field listed in index gets a lookup path, so a slug in an address finds its record directly.
  • List cache. For each field a page filters or sorts on, the values of all records are kept together in one cache file, so a page of 20 out of 50,000 reads 20 records.
  • Query cache. Query results are cached and invalidated on every write, whether it comes from the site, the API, Git or a sync.
  • Split directories. A folder with very many files slows the file system down. "splitdir": true in a resource's .meta spreads its records over two levels of subfolders by UUID, so every folder stays small. Turn it on for a resource that grows beyond about 10,000 records; nothing else changes, in templates, libraries or the API.

With split directories and indexes, a resource handles around 100,000 records. Writes to one resource happen one at a time, which keeps records consistent, also with several containers on a shared volume.

Designing resources with this in mind

  • One resource per kind of thing. Projects, people and sectors are separate resources; a select field refers from one to another.
  • Index what you look up. Slugs, email addresses, external ids.
  • Keep records focused. A record is read whole: put large or rarely used data in its own resource rather than in a growing field.
  • Use the system fields. Every record has uuid, _created, _modified and who made them; there is no need for fields of your own for those.

View this page as Markdown