Documentation/Start here
Your first project6 min read

Tutorial: first server, first change, second client

On this page

Twenty minutes from an empty directory to two people exchanging changes through one server. Everything runs on one machine over plaintext loopback, which needs no certificates or tokens. For a shared server, add TLS and authentication as described in auth.md; the client commands are the same with a tls:// address and a token file.

Scribe is centralized, so two Git habits translate to one command each: scribe submit is "commit and push" (a change exists only once the server has made it durable), and scribe sync is "pull" (the working copy is brought to the server's head). There is no local history to reconcile.

Build both binaries once:

sh
cargo build --release --locked

The examples below assume target/release is on PATH. Every output shown is real; ids and timestamps will differ on your machine.

1. Start the server

Pick a directory for the deployment. init creates the data root and a configuration file whose paths are relative to that file:

sh
mkdir demo && cd demo
scribe-server init --data-dir ./server-data --config-out ./server.toml --listen tcp://127.0.0.1:7447
text
initialized data root ./server-data
wrote configuration /home/ann/demo/server.toml

Run it. The command stays in the foreground; use a second terminal (or a service manager) for the clients:

sh
scribe-server serve --config ./server.toml
text
scribe-server: logging to ./logs (filter info from configuration)

The data root now holds the descriptor, the catalog, the ownership lock, and an empty repos/ directory. The runtime directory ./run holds the pid file and listen, which records the bound address. Logs go to ./logs; they are diagnostics only and are never needed for recovery.

Only one process may own a data root: a second serve, or any maintenance command such as doctor, fails on the lock while this one runs.

2. Connect a client and create a repository

The client finds the server through --server, SCRIBE_SERVER, or the address recorded in a workspace, defaulting to tcp://127.0.0.1:7447, which is what we chose above, so nothing needs to be set.

sh
scribe repo create game
scribe repo list
text
created: game id=5cf102e21bdd155313b1ee6ba0ace7a1 head=0 main=70d2d532f938 storage-compression=none
game                     head=0 id=5cf102e21bdd155313b1ee6ba0ace7a1

A workspace is a directory bound to one repository, one branch (main until you create others), and an author name. Create Ann's:

sh
scribe workspace create game ./ann --author ann
text
workspace fe49c4949a91
  root        /home/ann/demo/ann
  repository  game (5cf102e21bdd155313b1ee6ba0ace7a1)
  branch      70d2d532f938 (main)
  server      tcp://127.0.0.1:7447
  view        (entire repository)
  author      ann
  synced      change 0
  readonly    false
  compression none

From now on every scribe command run anywhere under ./ann finds this workspace by walking up; scribe workspace info prints the block above again. Large projects usually restrict a workspace with --view PREFIX so that only part of the tree is synced; leave it out here.

3. First change: add files and submit

Create some content, including a versioned ignore file:

sh
cd ann
mkdir -p Assets Source
printf 'hero mesh v1\n' > Assets/Hero.txt
printf 'fn main() {}\n' > Source/main.rs
printf 'target/\n*.log\n' > .scribeignore

Nothing is tracked until it is opened. add on a directory recurses and honours the ignore rules; status lists what is opened:

sh
scribe add .
scribe status
text
add .scribeignore
add Assets/Hero.txt
add Source/main.rs
scope: opened (synced to change 0)
add     present    .scribeignore
add     present    Assets/Hero.txt
add     present    Source/main.rs

Submit turns the opened files into one change. It hashes and chunks them, uploads only what the server lacks, and reports success only after the server has flushed the objects and the commit record to disk:

sh
scribe submit -m "Initial import"
text
submitted change 1 (3 files, 3 of 3 chunks sent, 607 object bytes sent for 40 logical bytes, 3 server flushes)

That is the whole "commit and push". scribe log shows it on the server:

text
change 1  2026-09-06T17:58:19.188Z  ann  3 entries
    Initial import

If the connection drops during a submit, the command exits with code 3 and keeps the candidate; the next scribe submit first asks the server whether change 1 was accepted and never creates a duplicate.

4. Second client: connect and pull

Bob works in another directory (on another machine he would point at the server with --server tcp://host:7447). A new workspace starts at change 0 with no files; sync brings it to head:

sh
cd ..
scribe workspace create game ./bob --author bob
cd bob
scribe sync
text
synced change 0 -> 1: 3 updated, 0 deleted, 0 unchanged (40 bytes fetched in 1 batches, 0 chunks from cache, 0 bytes reused locally) [full reconcile]
sh
find . -type f -not -path './.scribe/*' | sort
text
./.scribeignore
./Assets/Hero.txt
./Source/main.rs

Every file is verified against its content id before it is placed, and the workspace's watermark advances only after everything is in place, so an interrupted sync resumes from its journal rather than leaving a mix.

5. Round trip: Bob changes a file, Ann picks it up

Tracked files must be opened for edit before they change. Bob edits the hero and submits change 2; Ann syncs and receives it:

sh
# in ./bob
scribe edit Assets/Hero.txt
printf 'hero mesh v2\n' > Assets/Hero.txt
scribe submit -m "Hero v2"
text
edit Assets/Hero.txt
submitted change 2 (1 file, 1 of 1 chunks sent, 207 object bytes sent for 13 logical bytes, 2 server flushes)
sh
# in ./ann
scribe sync
cat Assets/Hero.txt
scribe log
text
synced change 0 -> 2: 1 updated, 0 deleted, 2 unchanged (13 bytes fetched in 1 batches, 0 chunks from cache, 0 bytes reused locally) [full reconcile]
preserved local Assets/Hero.txt at /home/ann/demo/ann/.scribe/recovery/1788717499825-0b42117921d692d35e1ab92bb94b74cc
hero mesh v2
change 2  2026-09-06T17:58:19.721Z  bob  1 entry
    Hero v2
change 1  2026-09-06T17:58:19.188Z  ann  3 entries
    Initial import

The "preserved" line is Scribe's rule that a sync never destroys bytes it is about to replace: the previous copy of the file is moved into .scribe/recovery/ first. That directory is yours to empty whenever you like. Had Ann opened the file for edit and changed it, the sync would have skipped it and listed it as deferred instead.

For binary assets that cannot be merged, take an exclusive lock before editing (scribe lock Assets/Hero.uasset); a submit from anyone else touching that path is rejected until scribe unlock.

6. Stop and inspect the server

Stop the server with Ctrl-C, SIGTERM, or the runtime file:

sh
touch ./run/shutdown-request

In-flight requests get a drain period; a forced kill is also safe because acknowledged changes are already durable. With the server stopped, the maintenance commands can take the lock:

sh
scribe-server usage --data-dir ./server-data
scribe-server doctor --data-dir ./server-data --quick
text
REPOSITORY    ID                HEAD     PATHS  BRANCHES    OBJECTS       LOOSE      PACKED    METADATA       TOTAL  LAST COMMIT
game          5cf102e21bdd         2         3         1         12         0 B       2 KiB     164 KiB     166 KiB  2026-09-06T17:58:19Z bob
1 repository, data root 1.2 MiB in 8 files (catalog 1.0 MiB)
filesystem: 623.63 GiB available of 1858.14 GiB (66% used) in 32 ms
text
doctor: healthy (1 repositories, 0 problem(s), 0 warning(s)) in 35 ms

usage (alias df) is the inventory; doctor (alias scan) is the read-only integrity check, and without --quick it re-hashes every object. Run doctor before every scribe-server backup --data-dir ./server-data --to <empty dir>.

Where next

Source docs/tutorial.mdSnapshot 93d02b17