shiply.now

Authentication

Add sign-in to your shiply-hosted app. shiply does not host customer auth — you bring your own Clerk (or Auth.js, Lucia, etc.) and store the keys as worker secrets.

Authentication

shiply hosts your site (static files + Workers Lite functions + databases). It does not host your customers' sign-in — that's an auth provider's job.

The recommended pattern: bring your own Clerk (or Auth.js, Lucia, whatever you like), store the keys as shiply secrets, and read them from your worker.

This page walks through Clerk because it's the fastest path. The same pattern works for any auth provider that ships JWT verification.

Why shiply doesn't manage your Clerk app

shiply has its own Clerk instance for signing agents and developers into the shiply dashboard. That instance is not multi-tenant for your customers' end users — mixing in someone else's app's users would be a security mess.

Spinning up your own Clerk app takes about two minutes and you own it forever. Your agent will do it for you the first time and store the keys in your shiply secrets. After that, every redeploy keeps working.

Set up Clerk for your app

1. Create the Clerk app (one-time, human or agent)

Visit dashboard.clerk.com → create a new application. Name it after your app. Pick the sign-in methods you want.

Copy two keys from the API Keys page:

  • Publishable key — starts with pk_test_… or pk_live_…. Safe to ship to the browser.
  • Secret key — starts with sk_test_… or sk_live_…. Server-side only. Never ship to the browser.

2. Store them in shiply

The secret key goes into a shiply secret (encrypted at rest, never reaches the browser):

shiply secret set <slug> CLERK_SECRET_KEY sk_live_xxxxxxxxxx

The publishable key can either go in a secret too OR get embedded in your build at publish time. If you bundle it in JS (which is normal for Clerk), you can just hard-code it in your source or set it as a build env var — it's designed to be public.

3. Use Clerk in your worker

worker.ts:

import { verifyToken } from '@clerk/backend'

interface Env {
  CLERK_SECRET_KEY: string
  ASSETS: Fetcher
  SITE_DB: D1Database
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url)

    // API routes: require a valid Clerk session token
    if (url.pathname.startsWith('/api/')) {
      const auth = req.headers.get('authorization')
      const token = auth?.replace(/^Bearer /, '')
      if (!token) return new Response('unauthorized', { status: 401 })

      try {
        const payload = await verifyToken(token, {
          secretKey: env.CLERK_SECRET_KEY,
        })
        // payload.sub is the Clerk user id — use it for D1 row scoping
        const userId = payload.sub

        if (url.pathname === '/api/me') {
          return Response.json({ userId })
        }
      } catch {
        return new Response('unauthorized', { status: 401 })
      }
    }

    // Static assets — Clerk's React SDK runs client-side and handles sign-in
    return env.ASSETS.fetch(req)
  },
}

4. Wire Clerk into your frontend

If you're building a React SPA, add Clerk's React SDK to your build:

npm i @clerk/clerk-react
// app.tsx
import { ClerkProvider, SignIn, SignedIn, SignedOut, UserButton } from '@clerk/clerk-react'

const PUBLISHABLE = 'pk_live_xxxxxxxxxx'  // safe to ship — it's public

export default function App() {
  return (
    <ClerkProvider publishableKey={PUBLISHABLE}>
      <SignedOut><SignIn /></SignedOut>
      <SignedIn>
        <UserButton />
        <YourApp />
      </SignedIn>
    </ClerkProvider>
  )
}

Then on every authenticated fetch, include the Clerk JWT:

import { useAuth } from '@clerk/clerk-react'

function YourApp() {
  const { getToken } = useAuth()
  const callApi = async () => {
    const token = await getToken()
    const res = await fetch('/api/me', {
      headers: { authorization: `Bearer ${token}` },
    })
    return res.json()
  }
}

5. Build + publish

shiply publish ./dist

shiply auto-deploys worker.ts if present and wires env.CLERK_SECRET_KEY from your secrets. Done.

Other providers

The pattern works the same with any provider that issues verifiable JWTs:

  • Auth.js / NextAuth — read the cookie, verify the JWE/JWT, scope queries
  • Lucia — store sessions in D1, look up the session cookie in your worker
  • Supabase AuthverifyJWT(token, env.SUPABASE_JWT_SECRET)
  • Firebase Auth — verify via Google's public certs

In every case: store the secrets via shiply secret set, read them as env.PROVIDER_KEY in your worker.

What shiply does NOT do

  • We don't run your customers' sign-up / sign-in flows
  • We don't OAuth into your auth provider's dashboard
  • We don't sync your customers' user records into our database
  • We don't issue tokens that your worker can verify (other than the agent API key)

Auth is one of the highest-stakes parts of an application. Owning it yourself — on your own Clerk/Auth.js/Lucia instance — is the right call.