A capability reference for elek.io Core - what the content engine does, grouped by the questions a developer evaluating a CMS tends to ask. Each entry links to the deep-dive doc where relevant.
For the data model these features operate on, see concepts.md.
Storage & data ownership
- Git-backed JSON storage - every Project is a git repository of plain, human-readable JSON files. Content is diffable, greppable and reviewable like code.
- No lock-in - there is no proprietary database. The files on disk are the source of truth. You can read, back up or migrate them without Core.
- Offline-first - Core runs fully locally and needs no server to create, edit or read content.
- Asset storage - binary Assets are stored via Git LFS (a pointer in history, the bytes in the local LFS store) plus a JSON sidecar holding metadata (size, MIME type, dimensions, etc.). See
git-and-sync.md.
Content modeling
- Collections - typed schemas (field definitions) every Entry must follow.
- Components - reusable, named bundles of field definitions shared across Collections. Updating a Component propagates to every Collection that references it.
- 18 field types across 6 value types (
string,number,boolean,reference,component,mdast) - including dedicated validated string types (email,url,ipv4,telephone), separatedate/time/datetime,range,select(string or number), and aslugtype that stores an always-unique, URL-friendly value. Seefields.md. - References -
assetandentryreference fields, polymorphic viaofCollections, with cardinality controlled bymin/max. - Polymorphic blocks - the
dynamicfield composes Components into page-builder-style content, restricted byofComponentsor fully open. - Layout & constraints - presentational field grouping, a 12-column
inputWidthgrid hint, and per-fieldisRequired/isUnique/isDisabled/defaultValue.isUniqueis enforced per language within a Collection on every write (seefields.md).
Internationalization
- Translatable by default - every direct value is multilingual out of the box. No plugin, junction table or special configuration.
- Project-scoped languages - each Project declares its supported languages, chosen from 24 supported language codes.
- Language-aware validation - content must carry a value for every language the Project supports. Missing translations fail validation.
- Narrowed generated types - generated clients and types narrow translatable content to the Project’s languages (
Record<ProjectLanguage, T>) rather than the broad superset.
Type safety & validation
- Schema-driven validation - Zod schemas are generated from field definitions and enforce structure, constraints and references at write time.
- Boundary validation - every public service method validates its input against a schema before running.
- Typed errors - failures throw a
CoreErrorcarrying a typed variant and HTTP status code. Seeerror-handling.md. - End-to-end TypeScript - schemas, services and generated artifacts are fully typed.
Versioning & history
- Full git history - read any object as it existed at any commit, not just the latest version.
- Migration on read - historical data is run through the migration chain on read, so it always comes back in the current schema shape. See
usage.md. - Transactional writes - create / update / delete operations are wrapped in an automatic git rollback that restores the working tree on failure. See
error-handling.md. - Releases - tagged snapshots of a Project at a point in time, managed through git tags.
- A version computed from the content - a Release diffs the draft against the released state and takes its semver bump from what changed, so a rename that breaks a consumer’s build arrives as a major. See
releases.md. - Branching - content edits happen on a
draftbranch, withmainreserved for released content.
Schema evolution
- Version-aware migrations - each entity type (Project, Asset, Collection, Component, Entry) has its own migration chain that transforms outdated files to the current schema.
- Project upgrade flow -
ProjectService.upgrade()migrates an entire Project and all its objects across Core versions on an isolated branch, then squash-merges the result.
Consuming content
- Programmatic API - a Node library (
ElekIoCore) exposing services with a consistent CRUD shape. Seeusage.md. - Local REST API - a read-only Hono + OpenAPI server with an interactive Scalar reference UI, for building static sites and apps against local content.
- Generated API clients - typed JavaScript / TypeScript clients (ESM or CJS) via
elek generate:client. - Generated TypeScript types - type definitions emitted directly from Project content models via
elek generate:types. - Astro integration - one
defineElekConfig()declaration names every Project a site consumes,elekCollections()derives an Astro collection per elek.io Collection from it, andelekAssetsLoader()/elekEntriesLoader()stay available for declaring collections by hand. Plus amdastRenderhelper, all from@elek-io/core/astro. - Provisioning - the
elek()Astro integration andelek provisionprovision Projects from their remotes into build environments, e.g. CI runners. Seeprovisioning.md. - JSON export - export Projects to JSON (nested or separate files) via
elek export, with a--watchmode for automatic re-exports.
Rich content
- Structured rich text - the
markdownfield stores a typed mdast tree, not an HTML blob or markdown string. - First-class references - Asset and Entry references are typed mdast nodes carrying UUIDs, not opaque URLs, so consumers resolve them however they like.
- Per-field feature allowlist - each
markdownfield controls exactly which node types (headings, tables, footnotes, raw HTML, links, images, etc.) are accepted. Disallowed nodes are rejected before they reach disk. - Safe by construction - exotic URL schemes (
javascript:,data:, …) are rejected at the schema layer, and a tree depth limit guards against pathological nesting. - Framework-agnostic rendering - an exhaustively-typed renderer primitive ships with an Astro binding. Other frameworks supply one handler per node type. See
markdown-content.md.
Collaboration & sync
- Any git provider - synchronize content by pushing and pulling against GitHub, GitLab, Bitbucket or any git remote that supports LFS.
- Remote management - set the remote origin, inspect commits ahead/behind, and synchronize through Core’s git service.
- Offline-first collaboration - work locally and sync when you choose. There is no always-on server in the loop.
Limitations
Core tries to keep a relatively small, predictable surface. Some of the items below are deliberate design choices and some are simply not built yet, so they are split accordingly.
Not yet implemented
- No full-text search - a
searchProjectSchemais defined, but there is nosearch()service method or API route behind it. - No conditional fields - a field’s visibility cannot depend on another field’s value.
- Limited specialized field types - no arbitrary JSON, color picker, geospatial, code-editor, or password / hash field types.
- Not every field property reaches a Release diff - some property and field type pairs are unclassified, so changing one of them alone leaves the bump at the patch default. See
releases.mdfor what a diff does carry.- All of them are restated in the generated types, so a patch Release can stop a build compiling. Regenerate and compile after any Release rather than only after a major, see
releases.md. - The pairs are a
markdownfield’sofCollections,ofAssetMimeTypes,features,minandmax, adynamicfield’sminandmax, anassetfield’sofAssetMimeTypes, and aselectfield’soptions. - The number variant of a
selectfield forcesminandmaxtonulland can never hold a bound, so a diff that reports no bound change for it is right rather than a gap. - A
dynamicfield’sofComponentsis compared as declared, so replacing an empty list with an explicit one is reported, even though an empty list already allows every Component.
- All of them are restated in the generated types, so a patch Release can stop a build compiling. Regenerate and compile after any Release rather than only after a major, see
Intentional constraints
- A Project is changed through Core only - git run by hand, another git client or an editor writing a Project’s files is not supported, and Core does not guard against it. Reading the files with any tool is safe. See
git-and-sync.md. - Git LFS requires an LFS-capable remote - Asset binaries are tracked with Git LFS, so the remote you push to must support it. A remote without LFS (for example a self-hosted server with it disabled) fails the push with a descriptive
PreconditionFailederror. Seegit-and-sync.md. - Field-definition
ids are caller-supplied - Core generates ids for entities (Collections, Components, Entries, Assets) but deliberately not for the field definitions inside them. You pass a UUID per field definition (for example viauuid()) on both create and update.- Generating them in Core was considered and rejected. It would need parallel id-less schema variants and only partly helps, because update must still identify existing field definitions by id.
- On update, field definitions are matched by
id. Send back theidof every field definition you want to keep and Core preserves the Entry data stored under it across slug renames and type changes. - A field definition whose
idis missing or changed counts as new, so the old one, and the Entry data keyed to it, is removed. Always round-trip the ids you read.
- One-way references only - no many-to-many, back-references, or reverse / virtual joins. The inverse of a reference is not maintained.
- Reference protection ignores field
defaultValues - an Asset or Entry reference embedded in a Collection or Component field’s markdowndefaultValueis not counted as a reference, because it only becomes a live reference once stamped into an Entry. - No computed or virtual fields - field values are static, with no derive-from-other-fields mechanism. The
slugfield’sofFieldDefinitionsis the one nuance: it is a soft generation hint the editor uses to pre-fill the slug from other fields, but the stored value stays static and user-editable, so Core never derives or binds it. - Uniqueness is single-field and Collection-scoped -
isUnique(and the always-uniqueslugtype) enforce that one field’s value is unique within a Collection, per language. There is no compound uniqueness such as “unique slug per category”, the per-language pairing is the only built-in composite.- Enforcement scans the Collection’s Entries on each write rather than reading a persisted index. That keeps it correct against Entries brought in by a pull or merge, but costs one read per existing Entry per write, so it scales poorly for very large Collections.
- No uniqueness inside Components -
isUniqueand theslugtype are rejected on Component field definitions. A Component is reused across Collections and can repeat within an Entry, so the scope of “unique” there is ambiguous; it is deliberately deferred rather than shipped with surprising semantics. - The data directory has to stay short on Windows - Windows resolves paths against a 260 character limit unless long paths are enabled. Core stores an Entry at
projects/<uuid>/collections/<uuid>/<uuid>.jsonand Git LFS stores its objects at a comparable depth, so it needs about 137 characters below the data directory.- A data directory beyond roughly 120 characters runs out of room, and file operations fail with
ENAMETOOLONG. - Core does not validate this. The limit depends on the platform and on whether long paths are enabled, so a fixed cap would reject setups that work. Keep
ELEK_IO_DATA_DIRshort instead, seeusage.md. - macOS and Linux allow 1024 and 4096 characters and are not affected.
- A data directory beyond roughly 120 characters runs out of room, and file operations fail with
- Astro dev picks up content changes but not model changes - the loaders reload Entries and Assets while
astro devruns, so editing content in the Desktop app updates the open page.- Editing the content model itself, a Collection’s field definitions, a Component or a Project’s languages, needs a dev server restart. Astro builds a collection’s schema and TypeScript types once when it loads the content config and offers no way to rebuild them in place.
- The loaders detect it and log what to do rather than reloading against a stale schema. See
usage.md.
- Astro’s
render()does not apply to Entries - amarkdownfield arrives onentry.dataas an mdast tree, not as the rendered HTMLrender(entry)and<Content />read, so the loaders leave Astro’s rendered slot empty.- Filling it would flatten
entryReferenceandassetReferencenodes into finished markup and lose the UUIDs a page resolves them by, which is the reason to store a tree in the first place. - Calling
render()on an Entry is not an error and renders nothing at all, so reach formdastRenderinstead. Seemarkdown-content.md.
- Filling it would flatten
- A major Release warns, it does not gate - a build that provisions the
releasechannel lands on the newest Release of a Project whatever its bump, so a major one reaches it like any other. A channel that stays within a major is not built yet. Pin an exact version to decide yourself when to move. Seereleases.md. - No plugin or marketplace system - extending the field catalogue means changing code, not installing a package.
- No nested composition - groups cannot nest groups, and
dynamicfields cannot nestdynamicfields. Components are the flat, predictable reuse mechanism. - A log tail can be truncated -
core.logger.tail()stops collecting at a size ceiling and setsisTruncated, dropping the oldest records first. Collapsing repeated records is what keeps a tail small, and it is worth far more than a bigger cap, so the ceiling is a safety valve rather than a working limit. Seeusage.mdfor the ceiling and whatisTruncatedmeans. - A log tail can be missing its last lines - winston hands a record to a write stream and offers no per transport flush, so a tail collected right after a crash can stop short of the lines it was collected for, which are usually the interesting ones.
tail()writes a marker and yields before reading to give the stream a chance to drain. That is a hedge rather than a fix, and the residual gap is documented instead of engineered around.
See also
concepts.md- the data model behind these featuresusage.md- how to use Core programmatically, via the API, CLI and Astrofields.md- full field type referencereferences.md- how references work end to end, including write-time and delete-time integrityschema-changes.md- how editing field definitions cascades into existing contentgit-and-sync.md- the branch model and synchronizing with a remotereleases.md- tagged snapshots and promotingdraftintomainasset-management.md- the two-file Asset modelstorage-layout.md- where Projects and their files live on disklocal-api.md- the read-only REST API referenceapi-clients.md- generating typed clients and TypeScript typesexport.md- exporting Projects to JSONmarkdown-content.md- richmarkdowncontent storage and renderingerror-handling.md-CoreErrorand error patterns