#StoryLark Repo Connection Standard v1
Status: normative. Deliverable D3 of the content standards plan.
Repo mode lets a deployment take its content from a git repository the customer already owns. This document defines what a connectable repository looks like, what StoryLark does on each sync, and — most importantly — what it will never do.
Prerequisites: content must conform to the StoryLark Content Format; the deployment must meet the deployment requirements, in particular §1.1 on outbound HTTPS.
#1. The premise
Git is the CMS. The repository is the source of truth; StoryLark is a reader of it. Content flows one way, always.
That single sentence settles most of the questions that follow. StoryLark holds a read-only token; it never commits, never opens a pull request, never writes a file back, and never asks the repository to change so that StoryLark can understand it. If a repository and this standard disagree, the repository wins and StoryLark reports what it could not use.
#2. What a connectable repository looks like
There is no required folder structure. The folder tree is organisation, never meaning. Filenames are not parsed, prefixes are not consulted, and depth carries no significance.
A repository is connectable when:
- It is reachable over HTTPS — public, or private with a read-only token.
- Content files carry a
storylark:block (SCF §2). Everything else in the repository is ignored, safely and by design. - Ids are unique within the connection, and
ordervalues do not tie (SCF §3.3, §3.4).
That is the whole requirement. A repository can be a manuscript folder, a website with content beside its templates, or a monorepo with StoryLark content in one subdirectory — all three work, because all three are answered by the same question: which files say they are StoryLark content?
#2.1 The path setting
path narrows where StoryLark looks. It is an optimisation and a scoping
tool, not a safety mechanism — the storylark: block is the safety mechanism.
Pointing at the repository root is a legitimate choice and ingests nothing
unintended.
#3. Connecting
The operator supplies:
| Field | Required | Notes |
|---|---|---|
| Provider | yes | GitHub at v1 — see §7 |
| Repository URL | yes | HTTPS |
| Visibility | yes | Public or private |
| Branch | no | Defaults to the repository's default branch |
| Path | no | Defaults to the root |
| Access token | private repos only | Read-only, scoped to this repository |
| Webhook secret | only for push-triggered sync | See §6 |
A public repository needs no credential. This is the cheapest way to evaluate repo mode, and it should be the path a first-time user takes.
#3.1 Authentication rules
HTTPS with a scoped, read-only token. Nothing else.
SSH is not supported, and the form says so explicitly. SSH means a keypair the deployment must hold and a host-key trust decision that does not belong in a web form. Declining it clearly beats appearing to offer it and failing obscurely at 3am.
The token never goes into committed config. This is a hard error, not a warning.
Two legitimate homes, in this order of preference. A platform secret (
CONTENT_SYNC_TOKEN), set by the installer, always wins when present. A token typed into the portal is stored in the deployment's own database.The second exists because of a real constraint: a default deployment cannot write its own platform secrets — that needs a deploy credential, and the recommended posture is not to hold one. A form that wrote a platform secret would have worked only on deployments configured for self-deploy, which are both the minority and the less locked-down ones. Operators who want the platform-secret path have it; operators who want to connect a repo from a browser are not forced into a weaker deployment posture to do so.
A stored secret may not be readable back — Cloudflare refuses by design — so the portal shows whether a credential is present and which home it is in, never what it is.
#3.2 Dry run
Connecting offers a dry run before anything is ingested. It fetches, validates every candidate through the same gate a real sync uses, and reports exactly what a real sync would do — without writing anything.
The dry run is not a different, gentler check. It is the same gate. That is the only way its verdict is worth anything.
#4. What a sync does
- Fetch the branch as an archive over HTTPS.
- Walk
path. - For every file with a
storylark:block, build a candidate record. - Hand every candidate to the content gate — as a set, not a stream (SCF §5), so file order never matters.
- Write what passed. Report what did not, with the reason and the line.
- Report what is in the library but was absent from this arrival (§5).
Step 4 is the whole architecture. The sync transport contains no validation logic of its own. A file rejected by a sync is rejected with the same code and the same message it would get from the portal or the API, and there is a test that asserts exactly this across all three doors.
#4.1 Partial success is the normal case
One bad file does not fail a sync. The good files publish; the bad ones are listed with their reasons. A repository being edited by humans will always have something in flight, and a sync that refuses everything because one file is mid-edit would be useless.
The exception is a whole-book rule: an order tie rejects that book,
because a book with an ambiguous reading order cannot be published coherently.
Other books in the same sync are unaffected.
#5. Deletion — what StoryLark will never do
A sync will never unpublish content on its own initiative.
A chapter in the library and absent from an arrival is reported as missing.
It stays published. The sync report offers a one-click removal, and a human
clicks it.
The reasoning matters more than the rule. A file's absence is not a statement. It is produced equally by an intentional deletion, a partial clone, a fetch that failed halfway, a renamed folder, a branch switch, and a force-push someone already regrets. Removing content after seeing an absence twice — an earlier proposal — infers intent from a pattern, and repeating an observation makes it a more confident guess, not a different kind of thing.
Removal, when asked for, is the ordinary recoverable delete: the library entry goes; content objects, source, and revision history remain.
#6. Triggers
| Trigger | Latency | Setup |
|---|---|---|
| Scheduled | Daily | None — on by default |
| Webhook | Seconds | A webhook secret in the provider |
| Sync now | Immediate | None — a button |
The scheduled sync runs on the job every deployment already has for update checks. No new infrastructure, nothing extra to provision.
#6.1 Webhooks
A webhook delivery is verified against the shared secret before it is believed. A forged or unsigned delivery is rejected and logged, and this is tested by asserting the rejection — a signature check that has only ever been tested with valid input has not been tested.
A webhook is an optimisation on top of the schedule, never a replacement for it. A missed delivery costs latency, not correctness, because the daily sync catches it. Any design where a dropped webhook loses content permanently is wrong.
#7. Providers
GitHub at v1, fetched as a repository archive over HTTPS. One provider, proven end to end, rather than three shipped on assumption.
A provider is exactly two functions:
| Function | Responsibility |
|---|---|
| Archive URL | Given repo, branch, and credential, produce a fetchable archive URL |
| Signature verifier | Given a delivery and the secret, say whether it is genuine |
That is the entire extension contract. Adding GitLab, Gitea, or Bitbucket is a driver, not a refactor — the same pattern the database and storage adapters already use. A provider driver contains no validation and no ingestion logic; if one grows some, the architecture has been broken.
#8. What the operator sees
Every connection reports, always:
- When it last synced, and whether that succeeded
- What it ingested, skipped, and why — per file, with line numbers
- What is missing (§5), and the button to act on it
- When it will next sync
- Whether a credential is present
A silent failure is a defect. A connection that stopped working must say so in the portal without anyone going looking for it — the operator should learn from StoryLark that a token expired, not from a reader asking where chapter twelve went.
#9. Boundaries
Not supported, deliberately, and each for a stated reason:
| Not supported | Why |
|---|---|
| SSH | §3.1 |
| Writing back to the repository | §1 — content flows one way |
| Bespoke per-customer parsers | The contract is the contract; SCF §2 replaces mapping |
Inferring content without a storylark: block |
SCF §2 |
| Auto-deleting on absence | §5 |
| Filename or prefix ordering | SCF §3.4 |
Each of these has been asked for, or will be. The answer is no, and the reason is written down so the answer stays the same next time.
Found a gap? StoryLark is open source — improve these docs on GitHub.