Deployment
copy-ink adds no infrastructure. It needs what a Next.js App Router site already needs, plus a few environment variables and a deployment that triggers on push.
Requirements
| Needs | Why |
|---|---|
| A Node.js runtime for the route handler | OAuth exchange and commits |
| Redeploy on push to your content branch | A publish is a commit |
| The env vars below | Sign-in and committing |
The route handler must run on Node, not Edge:
export const runtime = 'nodejs'
The rest of the site can be anything you like — static pages, ISR, streaming.
Static export
A statically exported site (output: 'export') can render content but cannot
use the GitHub backend: the OAuth code-for-token exchange needs a route handler.
copy-ink doctor reports this as static-export.
Environment variables
Set these in your host's project settings, for the production environment:
COPY_INK_GITHUB_CLIENT_ID=
COPY_INK_GITHUB_CLIENT_SECRET=
COPY_INK_GITHUB_APP_ID=
COPY_INK_GITHUB_PRIVATE_KEY=
COPY_INK_GITHUB_INSTALLATION_ID=
COPY_INK_SESSION_SECRET=
Full descriptions in the environment variable reference.
Preview deployments do not need them. Point previews at
backend: { type: 'local' } if you want the editor to work there without
writing to your repository — a local write on a read-only serverless filesystem
fails, so treat preview editing as a development convenience only.
Sessions on serverless
Set COPY_INK_SESSION_SECRET — openssl rand -base64 32 — before you hand a
site over.
Serverless hosting spreads requests across instances that share no memory, so a session held in one process is invisible to the next request. The symptom is nasty precisely because it is intermittent: the editor stops loading, or Publish returns 401, partway through a client's session, with no pattern they can describe.
With the secret set, the session is a signed cookie that any instance can verify, so there is nothing to share. Use the same value everywhere the site runs; changing it signs everyone out. Sessions explains why this is safe — the forge token is not in there, because nothing reads it after sign-in.
The redeploy loop
client hits Publish
→ route handler commits to the content branch
→ your host sees the push
→ build
→ deploy
→ the change is live
One to three minutes end to end, typically. The editor says "live in about 2 minutes" for that reason.
Two consequences worth designing around:
- Content changes cost a build. On a large site with a chatty client, that is a lot of builds. Incremental adoption of ISR helps; so does telling the client that Publish is for a batch of edits, not each one.
- A failed build means the change is committed but not live. Watch your deploy notifications; the commit is in git either way, so nothing is lost.
CI
check and i18n exit non-zero when they find errors, so they drop straight
into a workflow:
name: content
on:
push:
paths: ['content/**', 'copy-ink.config.ts']
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx copy-ink types
- run: npx copy-ink check
- run: npx copy-ink i18n
types runs first because check reads the manifest it writes.
A publish commit will trigger this workflow. That is the point: a client's edit goes through the same lint as yours, and a broken content file is caught before it reaches a build.
Branch protection
The GitHub App commits directly to backend.branch. If that branch is
protected against direct pushes, publishing fails with a BACKEND_ERROR.
Either exempt the App from the protection rule, or point backend.branch at an
unprotected branch that your production deployment tracks. Branch-and-PR
publishing is a v2 feature, not something to emulate by hand.
Checklist before handing over
npx copy-ink doctor
-
doctoris clean in the production environment -
COPY_INK_SESSION_SECRETis set, and the same on every instance - The GitHub App is installed on the repository, with Contents: write
- The OAuth callback URL matches the deployed domain
-
auth.allowlistcontains the client, not just you -
next.configimports.copy-ink/image-hosts.json - A test publish produced a commit and a deployment
- The client knows edits go live a couple of minutes after Publish