Skip to main content

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

NeedsWhy
A Node.js runtime for the route handlerOAuth exchange and commits
Redeploy on push to your content branchA publish is a commit
The env vars belowSign-in and committing

The route handler must run on Node, not Edge:

app/api/copy-ink/[...copyInk]/route.ts
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_SECRETopenssl 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:

.github/workflows/content.yml
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
  • doctor is clean in the production environment
  • COPY_INK_SESSION_SECRET is 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.allowlist contains the client, not just you
  • next.config imports .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