Victor A

5 min read

Signing in to Plain: a pasted token, or one click with GitHub

plain-cmsgithuboauthself-hosting

The boring little CMS I moved onto has a browser admin at /admin/. You write a post, hit Save, and it appears on your site. What's quietly unusual is what's not underneath it: no accounts, no passwords, no user database, no login server. There's nothing for me to run, and nothing for me to leak.

That's because in Plain, every "Save" is a Git commit. So "signing in" doesn't mean logging into Plain — there is no Plain to log into. It means proving to GitHub that you're allowed to write to the repository that holds your site. There are two ways to do that, and you can offer either (or both) on your own site. Here's how each works and how to set it up.

Option A — Paste a token (the two-minute way)

This is the one I started with, and for a site I run by myself it's still the one I'd reach for.

A GitHub fine-grained personal access token is a scoped key you generate once. You paste it into /admin/, and from then on the admin uses it to commit on your behalf.

Set it up:

  1. On GitHub: Settings → Developer settings → Fine-grained tokens → Generate new token.
  2. Repository access: only your site's repo — nothing else.
  3. Permissions: just Contents → Read and write. That's the whole grant. (That single permission is enough to publish, edit, and upload media.)
  4. Copy the token, open your /admin/, and paste it in alongside your owner/repo.

That's it — you're publishing.

The part I care about: the token lives only in your own browser (in localStorage), and it's only ever sent to api.github.com. It never touches my site, never hits a server I run, never appears in a URL. There's nothing in the middle to trust.

The catch is human, not technical: every writer has to generate and paste a token, and tokens can expire. For one person that's a shrug. For a team — or a client you'd rather not walk through GitHub's token screens — it gets awkward. Which is exactly what Option B fixes.

Option B — Sign in with GitHub (the one-click way)

This turns the sign-in screen into a real "Sign in with GitHub" button. A writer clicks it, approves once on GitHub, and they're in. No token to generate, nothing to paste.

It takes a bit more setup, because you're adding the one piece Plain deliberately doesn't ship: the little handshake that trades a GitHub login for a token. Two free ingredients:

  • A GitHub App — this is the permission. You create it once and pin it to Contents-only, on your content repo only. The token a writer gets can't do anything beyond that, on any other repo.
  • A tiny Cloudflare Worker — a stateless middleman that does the OAuth code-for-token exchange. It holds no database and no session; it takes the login GitHub hands back, passes the token straight to your admin page, and forgets it. It runs comfortably inside Cloudflare's free tier.

There is no central Plain login service in this picture. You own both pieces. I can't sign your writers in even if I wanted to.

Set it up (the short version — the repo has a full walkthrough):

  1. Create a GitHub App. Permissions: Contents: Read and write, Metadata: Read-only. Install it on your content repo.
  2. Deploy the Worker from the project's workers/oauth/ folder, and give it three secrets: the App's Client ID, a Client secret, and ALLOWED_ORIGIN — your site's address, the only place the Worker will ever hand a token.
  3. Point your site at it: add one line, "oauthUrl": "https://your-worker-url", to the site block of site.config.json, and rebuild.

On the next build, the sign-in screen grows a Sign in with GitHub button — and the paste-a-token form politely moves under "or use an access token." Both paths still work; you're adding a door, not replacing one.

The full step-by-step (including the exact GitHub App screens) lives in the project at workers/oauth/README.md.

One gotcha, since I just hit it. Each Worker serves exactly one site — ALLOWED_ORIGIN is a single origin on purpose, so a token can never be delivered to the wrong place. When I added the button to my second Plain site, I reused the same GitHub App but deployed a second Worker with its own origin. Don't try to point two sites at one Worker; the second one's sign-in will just quietly fail.

So which one?

  • Just you, one site, want it working before your coffee's cold? Paste a token. Two minutes, nothing to deploy.
  • A team, a client, more than one writer, or you just want it to feel like a product? Set up Sign in with GitHub. The upfront cost buys every future writer a one-click sign-in and no tokens to babysit.
  • Not sure? Start with a token today. The two coexist, so you can add the GitHub button later without disturbing anyone who's already signed in.

Either way, the thing I keep coming back to holds: there's no backend. Your content lives in Git, and now so does your notion of "who can publish." Nobody has to trust a server in the middle — because there isn't one.

If you haven't got your own copy yet, hit Use this template on github.com/plain-cms/plain, and pick whichever door suits you.