Inside .synck
Everything Synck knows about a repository lives in one directory, .synck.
Nothing is hidden elsewhere: no global database, no cache in your home folder,
no registry. This page opens it up, so you know what you are backing up and
why the rule is “look, but do not touch”.
Inspect freely, never modify. Reading these files, for example with
sqlite3 -readonly or shasum, is safe. Changing them in any way bypasses
every guarantee Synck makes.
What is in it
Every repository has the same history database. How content is stored depends on its storage mode, and a repository with a checkout has a few extra entries for its working tree:
.synck/
├── config.json format, identity, authority, storage mode
├── repository.sqlite3 history, current state and indexes
├── object-store.sqlite3 content as compressed chunks (CHUNKED)
├── objects/ content as hash-named files (WHOLE_FILE; empty otherwise)
├── lock
│
│ only with a checkout:
├── monitor.lock the optional background monitor
└── tmp/ partial writes
SQLite may also keep -wal and -shm files next to a database. They are part
of SQLite’s normal operation.
config.json
The configuration identifies the repository and states its format. A
repository created with synck init:
{"storage_mode":"CHUNKED","format_version":6,"repo_id":"01a1791d-0799-73f4-a7a2-33c189ca9e34","author":"Alex","authority":"INDEPENDENT_LOCAL","checkout":{"paths":[]}}
A repository freshly exported or copied:
{"storage_mode":"WHOLE_FILE","format_version":6,"repo_id":"01a174b7-8a7d-74e0-b0d7-81b1dcfa6ccd","authority":"FOLLOWER"}
authority is FOLLOWER for an export or copy, INDEPENDENT_LOCAL for a
repository created with synck init or after a
checkout, or LOCAL after
activation. A writer also records an
author, and a checkout records its selected paths; an empty list means
everything. A configuration without storage_mode means WHOLE_FILE. The file
never contains credentials, account details or absolute paths.
repository.sqlite3: the history
The history database has two families of tables. The vcs_ tables belong to
the engine and are the same in every Synck repository:
| Table | Holds | Authoritative? |
|---|---|---|
vcs_commits |
One row per commit: ID, author, committer, message, times, repository sequence | Yes |
vcs_events |
One row per item event: its item, item sequence, type and canonical JSON | Yes |
vcs_ref_events |
Events in items’ ref ledgers | Yes |
vcs_federation_domains |
Positions in externally sequenced (federated) streams | Yes, preserved exactly by copies |
vcs_items |
Each item’s current type, name, parent, lifecycle and latest sequences | Derived, kept in step with history in the same transaction |
vcs_repo_activity_heads |
The sequencer’s position | Derived |
vcs_state_nodes, vcs_state_roots, vcs_state_projection_heads |
The history index, a tree of snapshots for fast reads of old states | Derived, rebuildable |
A repository with a checkout adds local_ tables for its own bookkeeping, such
as which file on disk is which item and the result of the last scan. Copies
leave them out, and fsck checks them against the history too.
“Authoritative” means the history is those rows, together with the content
they reference. Everything else is derived from them and is checked against
them by fsck and repository verify. If derived state is damaged, it can be
rebuilt. If authoritative state is damaged, only a good copy can help.
You can read the events of any repository directly:
$ sqlite3 -readonly .synck/repository.sqlite3 "SELECT payload FROM vcs_events WHERE event_type = 'CHAT::MESSAGE::CREATE' ORDER BY item_seq LIMIT 1;"
{"base_seq":2,"event_type":"CHAT::MESSAGE::CREATE","item_id":"01a174b7-8a7d-74e0-b0d7-81d946ca6792","set_content":{"content_id":"sha256:91a93c8cd9ee5a004de7f179ff1b90e21a9ef254be46dddd3c6ba692d746e01e"}}
Whole-file content
With WHOLE_FILE storage, all content lives in one directory,
.synck/objects/. The sample repository uses it. Each file is named by the SHA-256 of its exact bytes, with no
subdirectories and no compression:
$ ls .synck/objects
47b256a9ac3c496db7fbb3208caf66fc30e821f5e31ea1f08a6f20472355a8a3
65d1490153c828ac1dde577ddd8a9806f0b4288e47e0f4541f0f8545a2ff51f8
91a93c8cd9ee5a004de7f179ff1b90e21a9ef254be46dddd3c6ba692d746e01e
c392b2570b38cf119c0b3f65c55d644ed468ce3edddb82a2887d4ba4ada53760
d6057b02c87e36a0294aa1c310e09a1c457a7d21a3aa16dd53066745a1900cd2
fdfdf64e89e31bbe76af72e4df33950552d618c1d77b2b2e76b79e857c6e8043
That makes the store checkable with nothing but standard tools:
$ shasum -a 256 .synck/objects/91a93c8cd9ee5a004de7f179ff1b90e21a9ef254be46dddd3c6ba692d746e01e
91a93c8cd9ee5a004de7f179ff1b90e21a9ef254be46dddd3c6ba692d746e01e .synck/objects/91a9…
Two kinds of content share this directory, and the reference in the event says which is which:
- A
blob_idrefers to a file’s raw bytes.d6057b02…isbudget.csv, exactly as it was saved. - A
content_idrefers to an encoded body: the text of a message, comment or post.
How a body is encoded
Here is Sam’s first message, the object that the event above refers to:
$ O=.synck/objects/91a93c8cd9ee5a004de7f179ff1b90e21a9ef254be46dddd3c6ba692d746e01e
$ wc -c < $O
89
$ head -c 18 $O
SYNCK_UTF8_BODY_V1
$ od -A d -t x1 -j 18 -N 33 $O
0000018 00 13 91 8d 98 d2 7a 7a 5a 41 76 88 d5 50 91 9f
0000034 5c 82 a8 04 49 55 4d 09 61 1a ab a3 2c eb 00 b6
0000050 7b
0000051
$ tail -c +52 $O
Please check the budget before Friday.
A body is three parts:
SYNCK_UTF8_BODY_V1 \0 │ 32 random bytes (salt) │ UTF-8 text
───── 19 bytes ────── │ ────── 32 bytes ────── │ the rest
The salt makes every body unique, even when two messages say the same thing. Without it, anyone holding a list of hashes could test whether a repository contains a message such as “yes” by hashing guesses. The cost is that identical messages are not deduplicated. The salt is not encryption. The text is in plain sight, as you just saw. Copies must keep the salt exactly, because the ID and every commit above it depend on it.
Which decoder applies is always decided by the reference type, never by
looking at the bytes. A file that happens to begin with SYNCK_UTF8_BODY_V1 is
still read as a file. If identical bytes are referenced both as a file and as a
body, they are stored once, and both checks apply.
Chunked content
With CHUNKED storage, the same content lives in a second database,
object-store.sqlite3, as chunks:
| Table | Holds |
|---|---|
blobs |
One row per stored file version or body, identified by the SHA-256 of its full bytes |
chunks |
Content-defined chunks, each identified by its own hash, compressed with zstd when that helps |
blob_chunks |
Which chunks, in which order, make up each file version or body |
store_identity |
Which repository this store belongs to |
The identities do not change: a blob_id or content_id is still the SHA-256
of the complete original bytes, and a body is still encoded exactly as above
before it is chunked. Reading reassembles the chunks and checks both every
chunk and the SHA-256 of the whole. Storage
space explains when each layout is
the better choice.
The other entries
lockis the file that commands lock to get exclusive access. It is always empty. Locks belong to running processes and vanish with them.monitor.lockbelongs to the optional background monitor.tmp/holds partial writes. Synck empties it at the start of each command.- Intent records such as
move.json,revert.jsonorbatch-commit.jsonappear only while an operation is in progress or was interrupted. They are how recovery knows what to finish.
Why not simply files?
It is fair to ask why a version-control tool keeps history in SQLite rather than in plain files, as Git does. The short answer is transactions. A commit must update the history, the current-state tables and the item sequences together, or not at all, and it must reject a stale change in the same step. SQLite gives exactly that, on every platform, in a single well-documented file format that any language can read. Content is kept apart from the history, either as plain hash-named files or in a separate chunk database, so a repository’s history can be read and checked without touching its largest part.