---
title: Keep the schema as files
description: "relate, the embedded Cypher graph database (relatedb.ai): Read a relate store's indexes and constraints from the text files in .cypherlite/schema/, review schema changes as diffs, and apply them to a new store."
type: how-to
surfaces:
  - cli
  - agents
status: stable
verified: 0.1.2
verified_by:
  - cli
canonical_url: https://relatedb.ai/docs/guides/storage/schema-as-files/
prerequisites:
  - get-started/install
related:
  - reference/storage/formats#directory-store-files
  - guides/agents/diffable-memory
  - guides/cypher/indexes-and-constraints
  - reference/cypher/schema-clauses
  - guides/storage/clone-and-convert
  - concepts/problems-relate-solves#schema-as-files
last_updated: 2026-11-09
---

# Keep the schema as files

Read a relate store's indexes and constraints from the text files in .cypherlite/schema/, review schema changes as diffs, and apply them to a new store.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

This guide shows you how to read a store's schema from files, review a schema change in a pull request, and apply the same schema to a new store. Every checkpoint of a durable store writes its indexes and constraints as Cypher text into the [schema folder](https://relatedb.ai/docs/reference/glossary/#schema-folder), `.cypherlite/schema/`, inside a directory store. The files are a copy for reading and diffing: the schema changes only through Cypher statements, and an edited file stops the store from opening.

## Prerequisites

- relate on your `PATH` ([Install relate](https://relatedb.ai/docs/get-started/install/)).
- A directory store. A `.rl` file keeps its contents inside one file, so to read its schema folder, clone it into a directory first ([Clone and convert a store](https://relatedb.ai/docs/guides/storage/clone-and-convert/)). To follow along, create the shared example graph as `team`:

```bash verify
cat > team.cypher <<'EOF'
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
EOF
relate query --database ./team < team.cypher
```

## Find the schema folder

The folder holds one file for a store without a schema, `schema.cypher`, and it is empty:

```bash verify
ls -A team/.cypherlite/schema
```

```text
schema.cypher
```

| File                               | Holds                                                                                                                                                                                                        | Present                                                                                                                                                                        |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `schema.cypher`                    | One `CREATE INDEX` or `CREATE CONSTRAINT` statement per line, as the `createStatement` column of `SHOW INDEXES` and `SHOW CONSTRAINTS` renders it: indexes first, then constraints, each group in name order | Always; empty when the store has no index or constraint                                                                                                                        |
| `graph-type.cypher`                | The store's [graph type](https://relatedb.ai/docs/reference/glossary/#graph-type), as the body of its declaration, such as ``(:`Person` => {`name` :: STRING NOT NULL})``                                    | When the store has a graph type                                                                                                                                                |
| `ontology.json` or `ontology.yaml` | The [ontology](https://relatedb.ai/docs/reference/glossary/#ontology) document, byte for byte as it was set                                                                                                  | When an ontology is set; the `db.ontology.*` procedures are a preview feature (`RELATE_PREVIEW=ontology`, [Use an ontology](https://relatedb.ai/docs/guides/cypher/ontology/)) |

A `markdown` store keeps the same folder beside its other engine files, in `.cypherlite/schema/` under the vault root. A constraint that a graph type manages, and an index that has no standalone `CREATE` form, is left out of `schema.cypher`; the graph type and the store's manifest carry it.

## Read the schema

Create a uniqueness [constraint](https://relatedb.ai/docs/reference/glossary/#constraint) and a range index. Each statement runs in its own process, and the CLI checkpoints when a run that opened the store for writing exits:

```bash verify
relate --database ./team --mode envelope "CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE"
relate --database ./team --mode envelope "CREATE INDEX person_role IF NOT EXISTS FOR (p:Person) ON (p.role)"
```

```text
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":1.233292,"execute_ms":1.121125,"lex_parse_ms":0.004583,"lower_ms":0.001,"parse_ms":0.106709,"plan_ms":0.001042,"result_generation_ms":0.000125,"rewrite_ms":0.000167},"truncated":false}
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":1.2614589999999999,"execute_ms":1.154958,"lex_parse_ms":0.003708,"lower_ms":0.000959,"parse_ms":0.101458,"plan_ms":0.001083,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.000167},"truncated":false}
```

One file read now answers what the schema is, with no store opened and no `SHOW` statement run:

```bash verify
cat team/.cypherlite/schema/schema.cypher
```

```text
CREATE RANGE INDEX `person_role` FOR (n:`Person`) ON (n.`role`);
CREATE CONSTRAINT `person_name` FOR (n:`Person`) REQUIRE (n.`name`) IS UNIQUE;
```

The folder changes at a checkpoint, not at each commit. A one-shot CLI run checkpoints as it exits. A long-running process, such as `relate serve` or an application on a client, checkpoints every 300 seconds by default, after 64 MiB of WAL or 10,000 commits, and when it closes the store; until then the folder shows the schema of the last checkpoint.

## Review a schema change

Commit the store folder, or at least `.cypherlite/schema/`, to the repository. A schema change then shows up in a pull request as added and removed lines. To see the same diff without git, keep a copy of the file, add a full-text [index](https://relatedb.ai/docs/reference/glossary/#index), and compare:

```bash verify
cp team/.cypherlite/schema/schema.cypher schema-before.cypher
relate --database ./team --mode envelope "CREATE FULLTEXT INDEX project_search IF NOT EXISTS FOR (p:Project) ON EACH [p.name, p.language]"
diff schema-before.cypher team/.cypherlite/schema/schema.cypher
```

```text
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":0.9976670000000001,"execute_ms":0.89025,"lex_parse_ms":0.0041659999999999996,"lower_ms":0.001042,"parse_ms":0.10291600000000001,"plan_ms":0.000917,"result_generation_ms":0.000375,"rewrite_ms":0.000167},"truncated":false}
1a2
> CREATE FULLTEXT INDEX `project_search` FOR (n:`Project`) ON EACH [n.`name`, n.`language`] OPTIONS {indexConfig: {`fulltext.analyzer`: 'standard-no-stop-words',`fulltext.eventually_consistent`: false}};
```

The rendered statement spells out every option, defaults included, so a reviewer sees every setting the index runs with. An agent reads the same file to learn which indexes a statement can use before it writes one.

## Apply the schema to a new store

Each line of `schema.cypher` is a statement that ends with `;`, so the file runs as a script. Pipe it into a new, empty store to give that store the same indexes and constraints, then compare the two copies:

```bash verify
relate query --database ./team-staging --mode envelope < team/.cypherlite/schema/schema.cypher
cmp team/.cypherlite/schema/schema.cypher team-staging/.cypherlite/schema/schema.cypher && echo "same schema"
```

```text
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":1.1729170000000002,"execute_ms":1.0586659999999999,"lex_parse_ms":0.002959,"lower_ms":0.000833,"parse_ms":0.109833,"plan_ms":0.001,"result_generation_ms":0.00029099999999999997,"rewrite_ms":0.000166},"truncated":false}
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":0.855208,"execute_ms":0.804625,"lex_parse_ms":0.003167,"lower_ms":0.000833,"parse_ms":0.049459,"plan_ms":0.00075,"result_generation_ms":0.000041,"rewrite_ms":0.000167},"truncated":false}
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":0.8208329999999999,"execute_ms":0.79225,"lex_parse_ms":0.002208,"lower_ms":0.0007080000000000001,"parse_ms":0.027375,"plan_ms":0.0007080000000000001,"result_generation_ms":0.000041,"rewrite_ms":0.000084},"truncated":false}
same schema
```

Apply the schema before loading data, so a uniqueness constraint checks every row as it arrives. The statements in the file carry no `IF NOT EXISTS`, so running them against a store that already has the same index fails at the first one ([If it goes wrong](#if-it-goes-wrong)).

## Keep the files read-only

The folder is a mirror of the schema the last checkpoint wrote, never a source. Each open compares it byte for byte with that schema, and a changed, missing or extra file fails the open, read-only included. An index added by hand to the file is refused:

```bash verify
cp -R team team-edited
echo "CREATE INDEX person_age FOR (n:Person) ON (n.age);" >> team-edited/.cypherlite/schema/schema.cypher
relate --database ./team-edited --read-only --mode envelope "RETURN 1 AS ok"
echo "exit=$?"
```

```text
Failed to open Relate: storage error: schema mirror .cypherlite/schema/schema.cypher differs from the schema this store's checkpoint persisted; it was edited by hand or copied from another store; delete the .cypherlite/schema/ directory and reopen: the next checkpoint regenerates it from the store's schema
{"error":{"code":"fatal_error","exit_code":1,"kind":"storage","message":"storage error: schema mirror .cypherlite/schema/schema.cypher differs from the schema this store's checkpoint persisted; it was edited by hand or copied from another store; delete the .cypherlite/schema/ directory and reopen: the next checkpoint regenerates it from the store's schema","retryable":false}}
exit=1
```

The refusal touched nothing else in the store. The repair is in the message: delete the folder and open the store for writing once, so its checkpoint at exit writes the folder again from the store's schema:

```bash verify
rm -r team-edited/.cypherlite/schema
relate --database ./team-edited --mode envelope "RETURN 1 AS ok"
cat team-edited/.cypherlite/schema/schema.cypher
```

```text
{"columnTypes":["INTEGER"],"columns":["ok"],"notifications":[],"planCache":"miss","rows":[{"ok":1}],"timings":{"elapsed_ms":0.463208,"execute_ms":0.34041699999999997,"lex_parse_ms":0.019625,"lower_ms":0.027542,"parse_ms":0.080667,"plan_ms":0.033541999999999995,"result_generation_ms":0.000084,"rewrite_ms":0.0067079999999999996},"truncated":false}
CREATE RANGE INDEX `person_role` FOR (n:`Person`) ON (n.`role`);
CREATE FULLTEXT INDEX `project_search` FOR (n:`Project`) ON EACH [n.`name`, n.`language`] OPTIONS {indexConfig: {`fulltext.analyzer`: 'standard-no-stop-words',`fulltext.eventually_consistent`: false}};
CREATE CONSTRAINT `person_name` FOR (n:`Person`) REQUIRE (n.`name`) IS UNIQUE;
```

The hand-added `person_age` index is gone: to add an index, run its `CREATE INDEX` statement. A schema change committed since the last checkpoint is not drift; it waits in the WAL, and the next checkpoint writes it to the folder. A store with no `.cypherlite/schema/` folder at all, such as one written by an older release, opens normally and gains the folder at its next checkpoint.

## Diff the data too

The schema folder covers indexes and constraints. To review changes to nodes and relationships as well, keep the store in the `json` codec, whose checkpoint writes the whole graph to one `graph.json` beside the same schema folder:

```bash verify
relate clone --to-snapshot-format json ./team ./team-json
ls -A team-json team-json/.cypherlite/schema
```

```text
team-json:
.cypherlite
.cypherlite.lock
.cypherlite.writer
graph.json
manifest.json
wal

team-json/.cypherlite/schema:
schema.cypher
```

`graph.json` is written as one line of JSON, so a line diff marks the whole file as changed; pass it through a JSON formatter before diffing to see which nodes and relationships changed. [Keep a diffable memory in the repo](https://relatedb.ai/docs/guides/agents/diffable-memory/) applies this to an agent's memory, and the [formats reference](https://relatedb.ai/docs/reference/storage/formats/#codecs) shows what `graph.json` holds.

## Verify

The store's own catalog lists the same three entries the file holds. `SHOW INDEXES` includes the range index that backs the uniqueness constraint, under the constraint's name:

```bash verify
relate --database ./team --read-only --mode envelope "SHOW INDEXES YIELD name, type RETURN name, type ORDER BY name"
```

```text
{"columnTypes":["STRING","STRING"],"columns":["name","type"],"notifications":[],"planCache":"miss","rows":[{"name":"person_name","type":"RANGE"},{"name":"person_role","type":"RANGE"},{"name":"project_search","type":"FULLTEXT"}],"timings":{"elapsed_ms":0.574083,"execute_ms":0.419833,"lex_parse_ms":0.027875,"lower_ms":0.044042000000000005,"parse_ms":0.106791,"plan_ms":0.037167,"result_generation_ms":0.000375,"rewrite_ms":0.007625},"truncated":false}
```

## If it goes wrong

- **`Failed to open Relate: storage error: schema mirror .cypherlite/schema/<file> differs from the schema this store's checkpoint persisted; …`**, **`… schema mirror .cypherlite/schema/<file> is missing from the mirror this store's checkpoint wrote; …`** or **`… schema mirror .cypherlite/schema/<file> is not part of this store's schema mirror; …`** (exit code 1). A file in the folder was edited, deleted or added, or copied from another store, or a crash interrupted a checkpoint between writing the store and rewriting the folder. Delete `.cypherlite/schema/` and open the store for writing once; nothing in the store is lost.
- **`semantic error: IndexAlreadyExists`** (exit code 6) when piping `schema.cypher` into a store. The store already has an index of that name; the script stops at the first failing statement and runs none after it. Apply the file to an empty store, or run only the statements for the entries the store lacks.
- **The folder shows an old schema.** A long-running process has not checkpointed since the change. The folder catches up at its next checkpoint, or when the process closes the store.

## Related

- [Storage formats](https://relatedb.ai/docs/reference/storage/formats/#directory-store-files): every file a directory store holds.
- [Indexes and constraints](https://relatedb.ai/docs/guides/cypher/indexes-and-constraints/): the statements that change the schema.
- [Schema clauses](https://relatedb.ai/docs/reference/cypher/schema-clauses/): the `CREATE`, `DROP` and `SHOW` forms.
- [Keep a diffable memory in the repo](https://relatedb.ai/docs/guides/agents/diffable-memory/): a store an agent writes, reviewed in pull requests.
- [Schema as files](https://relatedb.ai/docs/concepts/problems-relate-solves/#schema-as-files) in the capability catalog.

## Terms used on this page

- **schema folder**: The text copy of a store's schema under `.cypherlite/schema/`: indexes and constraints, the graph type and the ontology, rewritten at each checkpoint.
- **graph type**: A schema for a whole graph, declared in Cypher, that lists node and relationship types with their properties; writes that break it fail.
- **ontology**: An Ontology Graph Spec document stored with a store, managed by the `db.ontology.*` procedures behind the `ontology` preview, and appliable as the graph type.
- **constraint**: A rule the store enforces on every write, such as a unique property, a required property, a property type or a node key; a violating write fails.
- **index**: A structure the planner uses to find nodes or relationships by a property without scanning a label: range, text, point, vector or full-text.
