#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 What counts as connected
A repository appears under Admin → Connections only after an operator uses Connect a repo, the dry run passes, and StoryLark saves the connection in the deployment database. That saved record is what gives StoryLark enough information to display status and operate scheduled sync, webhooks, and Sync now.
A GitHub Actions workflow may also read a repository and push content through the StoryLark content API. That is a valid external publishing design, but the workflow — not StoryLark — owns the repository access. Content is recorded as API/external, and no Admin repo connection is created. The Connections screen must not infer or fabricate one from public content metadata.
#3.2 Authentication rules
HTTPS with a scoped, read-only token. Nothing else.
Private-repo credentials must be durable enough for the configured schedule. A short-lived GitHub App installation token can authenticate one workflow run, but it expires and is not a saved connection credential.
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.3 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.2 |
| 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.