#StoryLark deployment & credential requirements v1
Status: normative. Deliverable D4 of the content standards plan.
This document answers one question: what must a deployment provide for StoryLark's content features to work? It is written for the person deciding whether StoryLark fits their infrastructure, before they commit to trying it.
Companion documents: the StoryLark Content Format (what content must look like) and the content API (how it arrives).
#1. The shape of the requirement
StoryLark runs on a customer's own infrastructure. It needs five capabilities, and it does not care which vendor supplies them:
| Capability | Why | Cloudflare | Azure |
|---|---|---|---|
| A JavaScript runtime for the API | Serves the app and every content route | Workers | App Service (Node) |
| A SQL database | Accounts, sessions, progress, connections | D1 | PostgreSQL Flexible Server |
| Object storage with public read | Chapter JSON, audio, images | R2 | Blob container |
| A scheduled trigger | Update checks and content sync | Cron trigger | Timer |
| A secret store the runtime can read | Repo tokens, API keys | wrangler secret |
App settings |
Two platforms are supported out of the box. A third is a driver, not a port — the database and storage seams already exist, and the Postgres and S3-API drivers between them cover most of what AWS would need.
#1.1 One capability that is easy to overlook
The runtime must be able to make outbound HTTPS requests.
This is what makes repo sync possible at all. A Worker cannot shell out to
git, so StoryLark fetches a provider's archive over HTTPS and unpacks it in
process. Any runtime that can fetch a URL can sync a repository; any runtime
that cannot, cannot — regardless of what else it offers.
#2. What the operator must supply
#2.1 Always
| Value | Notes |
|---|---|
| Brand id | Which brand's presentation this deployment serves |
| App origin | Where the app is served from |
| Content origin | Where content is served from — a plain URL, deliberately not a binding |
| Mail sender | Used for password reset and account mail |
| Admin credential | Bootstraps the first admin account |
These live in a gitignored env file per platform, which is the installer's contract. The installer has a verify mode that sanity-checks the values before creating anything.
#2.2 Only for repo mode
| Value | Notes |
|---|---|
| Repository URL | HTTPS |
| Branch | Defaults to the repository's default branch |
| Path | Where content lives inside the repo; the root is fine |
| Access token | Only for a private repository |
| Webhook secret | Only if push-triggered sync is wanted |
A public repository needs no credential at all, which is worth stating plainly: the cheapest way to try repo mode is a public repo and no secrets.
#2.3 Only for API mode
A scoped content-API token, issued from the portal. Nothing else.
#3. Credential handling — the rules
These are requirements, not recommendations.
- A repository token never goes into
deployment.jsonor any committed file. This is already a hard error rather than a warning, and it stays one. It has two legitimate homes: a platform secret (CONTENT_SYNC_TOKEN), which is preferred and always wins when present, or the deployment's own database when an operator types one into the portal — because a default deployment cannot write its own platform secrets without holding a deploy credential it is better off not holding. - SSH is not supported, and the UI says so. SSH means a keypair the deployment must hold and a host-key trust decision nobody wants to make in a web form. HTTPS with a scoped token is the supported path, and declining SSH explicitly is better than appearing to offer it and failing obscurely.
- Scope the token to reading one repository. StoryLark never writes to a connected repository, so a token that can is a liability with no upside.
- A secret written to a platform store may not be readable back. Cloudflare refuses to return a secret's value by design. Any flow that assumes it can re-read what it wrote is wrong on at least one supported platform, so the portal shows whether a credential is present, never what it is.
- Rotation is the operator's, and it must not need us. Replacing a token is writing a new secret and saving the connection. No support ticket, no redeploy.
#4. Scheduling
Every deployment already runs a scheduled job for the engine update check. Content sync is a second job on that same schedule — no new infrastructure, nothing extra for the operator to provision or understand.
The default is daily. Three tiers cover every real need:
| Tier | Latency | Requires |
|---|---|---|
| Webhook | Seconds | A webhook secret, and a provider that sends them |
| Scheduled | Up to a day | Nothing — it is on by default |
| Sync now | Immediate | Nothing — a button in the portal |
Hourly polling was considered and rejected: it would change the schedule on every deployment and multiply API-rate spend by twenty-four, to serve a case webhooks already serve better.
#5. Network
| Direction | Requirement |
|---|---|
| Inbound to the app | Public HTTPS. Readers use it. |
| Inbound to the content origin | Public HTTPS, read-only. |
| Inbound webhook route | Public, if push-triggered sync is used. Signature-verified, and a forged signature is rejected. |
| Outbound from the runtime | HTTPS to the repository provider. Without this, repo mode cannot work (§1.1). |
A deployment behind a network that blocks outbound HTTPS can still use the portal and the content API. It cannot use repo mode, and this is the one capability whose absence removes a whole feature rather than degrading it.
#6. What StoryLark does not require
Worth stating, because these are the assumptions that usually make a self-hosted tool unusable:
- No dedicated server. Both supported platforms are serverless or managed.
- No container registry, no Kubernetes.
- No git binary on the runtime — see §1.1.
- No inbound access to the customer's own systems. StoryLark pulls; nothing needs to reach into a private network to push.
- No write access to anything of the customer's. Repo tokens are read-only, and content flows one way.
- No CMS. StoryLark deliberately does not ship one and does not require one.
#7. Sizing
Four chapters across the two reference deployments today, so nothing here is load-bearing yet and these are engineering estimates rather than measurements. They are stated as ceilings the contract enforces, which is the honest version.
| Dimension | Limit |
|---|---|
| Chapter source | 2 MB, enforced by the content gate |
| Ids | 64 characters |
| Storage | Chapter JSON is small; narration audio dominates by roughly two orders of magnitude |
| Database | Accounts, sessions, progress, connections. Small. |
The planning number that matters: audio is the storage cost. A text-only library is negligible on any platform's free tier.
#8. Verifying a deployment before trusting it
A deployment satisfies this document when all of the following are true. Each is observable, deliberately — an operator should be able to check without asking us:
- The app answers on its origin, and its health endpoint reports the expected brand.
- Content is served from the content origin and is publicly readable.
- An admin can sign in and reach the portal.
- A chapter published through the portal appears in the library.
- The scheduled job has run at least once, visibly.
- For repo mode: a Sync now on a connected repository completes and reports what it ingested, what it skipped, and why.
- For repo mode with webhooks: a correctly-signed delivery syncs, and a forged one is rejected. Test the rejection — a webhook route that accepts anything is worse than no webhook route.
#Appendix — status of the two reference deployments
Measured 2026-08-17. Recorded because a reference deployment that quietly fails a requirement makes this document a work of fiction.
| Requirement | Cloudflare demo | Azure dev |
|---|---|---|
| Runtime | ✅ | ✅ health 200 |
| Database | ✅ D1 | ✅ Postgres |
| Public content | ✅ R2 via content.storylark.dev |
✅ blob container |
| Scheduled trigger | ✅ | ✅ |
| Secret store | ✅ | ✅ |
| Narration | ✅ two voices | ❌ none — the path has never run here |
Two open items on Azure, neither blocking this document: narration has never run end to end there, and the storage account holds two containers with identical contents, one presumably orphaned. Both are recorded in the design's §6.7.
Found a gap? StoryLark is open source — improve these docs on GitHub.