A working backend — auth, database, storage — on your own Cloudflare account, in about five minutes.
You'll need a Cloudflare account and a scoped API token. The token is the only secret Flarelink ever sees — your D1, R2, and Worker config all live on your account, not ours.
1. Connect your Cloudflare
Sign up at dash.flarelink.dev, then create a scoped Cloudflare API token with these permissions:
Account → Workers Scripts: Edit
Account → D1: Edit
Account → Workers KV Storage: Edit
Account → Workers R2 Storage: Edit
User → API Tokens: Edit (so Flarelink can mint a scoped R2 keypair for you)
Paste the token on /connect-cf. Flarelink validates it against your account before storing it AES-256-GCM encrypted.
2. Provision a project
Click New project. The wizard provisions everything in one atomic operation:
A D1 database for your users (with the auth schema applied)
A KV namespace for sessions
An auth Worker uploaded to your account on *.workers.dev
A service key for the SDK, shown exactly once
R2 storage is optional — you can attach it later from the project's Storage panel when you need to handle files. If any provisioning step fails, the whole thing rolls back; you won't end up with half a backend.
The service key is shown exactly once in the secret-bundle modal. Copy it somewhere safe before dismissing — Flarelink stores only a SHA-256 hash and can't recover the plaintext. Lost it? Hit Rotate service key on the project's Auth panel.
3. Install & configure
Add @flarelink/client to your app — works on Node, Bun, Cloudflare Workers, Vercel, anywhere.
npm install @flarelink/client
Add the auth Worker URL and the service key to your env. The naming below matches the rest of these docs — use whatever your framework prefers.
# .env / .env.local / .dev.vars
FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev
FLARELINK_SERVICE_KEY=flarelink_sk_… # server-only, NEVER in browser bundles
4. Set trusted origins (don't skip)
In your project's Auth panel, add every origin your app runs on — production, staging, http://localhost:3000 for dev. The Worker rejects requests from anywhere else with a 403.
This is the most common misconfiguration. If sign-in returns 403 with MISSING_OR_NULL_ORIGIN, add your origin and try again.
5. Use it
In the browser, only flarelink.auth.* is callable — no service key.
That's it — the rest of this page is reference + framework-specific starters.
SDK reference
Every public method on @flarelink/client, mirroring the package README. Types are accurate as of v0.2.0.
Auth
Browser + server safe. Every call sends credentials: 'include', so the browser carries the session cookie automatically. On the server, you'll need to forward cookies manually — see SSR cookies.
// Sign up / sign inawait flarelink.auth.signUp({ email, password, name })
await flarelink.auth.signIn({ email, password })
// OAuth — redirects to provider by defaultawait flarelink.auth.signInWithSocial("google")
const { url } = await flarelink.auth.signInWithSocial("github", { noRedirect: true })
// Magic linkawait flarelink.auth.signInWithMagicLink("user@example.com")
// Sign outawait flarelink.auth.signOut()
// Who's signed in?const user = await flarelink.auth.getMe() // User | nullconst session = await flarelink.auth.getSession() // Session | null// Password reset (two steps)await flarelink.auth.requestPasswordReset({
email: "user@example.com",
redirectTo: "https://myapp.com/reset", // your reset page
})
// …user clicks the email link, your page reads ?token=await flarelink.auth.resetPassword({ token, newPassword })
// Email verification (manual trigger)await flarelink.auth.sendVerificationEmail({ email })
For IN (…), ranges (> / < / LIKE), OR, joins, transactions — use the tagged template. Interpolated values become bind params; there's no way for a value to inject SQL.
const { rows: top } = await flarelink.sql<{ email: string; n: number }>`
SELECT email, count(*) AS n
FROM events
WHERE created_at > ${cutoff}
GROUP BY email
ORDER BY n DESCLIMIT 10
`
Limits & gotchas
Builder supports equality + AND only. Anything more dynamic goes through flarelink.sql\`…\`.
Identifiers (table + column names) must match /^[A-Za-z_][A-Za-z0-9_]*$/ — anything else throws INVALID_IDENTIFIER before the request is sent.
Flarelink's auth tables share the same D1: user, account, verification, flarelink_config. Avoid using those names for your own tables — you can read them like any other (flarelink.from('user')).
Browser-side queries with row-level security policies are deferred to a later release.
Storage
Server-only SDK calls — every method needs the service key. But uploads and downloads themselves go browser → R2 directly via presigned URLs. Your server hands out short-lived URLs; the browser uses them to talk to R2. Zero bytes through Flarelink or your own server.
Presigned upload
// SERVER — mint a URL, return it to the browser via your APIconst { url, signedHeaders } = await flarelink.storage
.from("uploads")
.createSignedUploadUrl("avatars/jane.png", {
contentType: "image/png",
expiresIn: 600, // optional; default 300s, clamped to [60, 3600]
})
// BROWSER — PUT direct to R2awaitfetch(url, {
method: "PUT",
headers: signedHeaders, // don't add extras — it'll break the signature
body: file, // File | Blob | ArrayBuffer
})
Presigned download
const { url } = await flarelink.storage
.from("uploads")
.createSignedDownloadUrl("avatars/jane.png")
// Use it anywhere a URL works: <img src={url} />, window.open(url), fetch(url)…
Server-only operations
// Delete one or more objectsawait flarelink.storage.from("uploads").remove(["avatars/old.png"])
// List objects under a prefix (up to 1000 per call; page via cursor)const { objects, prefixes, nextCursor } = await flarelink.storage
.from("uploads")
.list({ prefix: "avatars/" })
// All buckets on this project's R2 accountconst buckets = await flarelink.storage.listBuckets()
SSR: forwarding the session cookie
In server frameworks (Next.js, SvelteKit, Remix, …) the browser's session cookie isn't on the server fetch by default — so flarelink.auth.getMe() would return null from a route handler / loader even when the user is signed in. Pass a cookies function and the SDK does the rest:
// Next.js (App Router)import { cookies } from"next/headers"import { createFlarelink } from"@flarelink/client"const flarelink = createFlarelink({
url: process.env.FLARELINK_AUTH_URL!,
serviceKey: process.env.FLARELINK_SERVICE_KEY!,
cookies: () => cookies().toString(),
})
// flarelink.auth.getMe() now returns the signed-in user from your route handler
cookies is called per-request, so it's safe to define the client at module scope when your framework's cookie API is request-scoped. Pass a plain string instead of a function if cookies are static. No effect in the browser — credentials: 'include' already carries cookies there. Use fetch: for full control if you need it (e.g. tests).
Errors
Every failure throws a typed class with .status (HTTP code) and a machine-readable .code. Branch on .code, not on message strings.
import { AuthError, StorageError, DatabaseError, MissingServiceKeyError } from"@flarelink/client"try {
await flarelink.auth.signIn({ email, password })
} catch (err) {
if (err instanceof AuthError && err.code === "INVALID_PASSWORD") {
// show "wrong password" in the UI
}
throw err
}
AuthError — auth failures. Common codes: INVALID_PASSWORD, USER_NOT_FOUND, TOO_MANY_REQUESTS, MISSING_OR_NULL_ORIGIN.
StorageError — storage failures. Common codes: INVALID_SERVICE_KEY, SERVICE_KEY_NOT_PROVISIONED, R2_NOT_CONFIGURED.
DatabaseError — DB failures. Common codes: INVALID_IDENTIFIER, UNSUPPORTED_FILTER, D1_QUERY_FAILED.
MissingServiceKeyError — you called flarelink.storage / flarelink.from without passing serviceKey to createFlarelink.
Guides
Configure email
Open the Email panel on your project. Pick Cloudflare Email Sending or Resend, paste the from-address (and the Resend API key if applicable). The auth Worker picks it up within a second — password reset, magic links, and verification all become live.
Custom domain for auth
The auth Worker ships on {project}.workers.dev by default. To bind it to auth.yourdomain.com:
Open the project's Auth panel and click Attach custom domain
Pick the zone and subdomain from the dropdown — TLS cert issues automatically
Update your OAuth providers' redirect URIs to the new hostname (Flarelink lists them for you)
Only zones on Cloudflare's nameservers work today — if your domain is on another DNS provider, you'll need to migrate the zone to CF first.
Rotating R2 credentials
If your R2 access key leaked or you just want to rotate on a schedule:
Open Files in the sidebar.
In the Buckets column header, click the gear icon (manage credentials) — it sits next to the link and plus icons.
In the R2 credentials modal, click Regenerate and confirm the prompt.
Under the hood Flarelink does a mint-then-revoke: a fresh keypair is minted via your Cloudflare API token, validated against R2, written to the project, and only then the previous Flarelink-managed token is revoked. The new accessKeyId + secretAccessKey are shown exactly once in a secret modal — copy them somewhere safe.
The new keys are also fanned out to every auth Worker on the project (their flarelink_config is updated and the Worker is pinged to reload), so server-side flarelink.storage.* calls switch over within a second.
Pasted credentials (you brought your own R2 keypair instead of letting Flarelink mint one) only get Clear credentials — there's no Regenerate button. Revoke the old token in the Cloudflare dashboard, then re-open the modal and paste a new keypair.
In-flight presigned URLs signed with the old keys will fail after the old token is revoked. Rotate during a quiet window if you have heavy traffic.
If the dashboard reports any sync failures after rotation (a Worker couldn't be reload-pinged), give it 60s — the Worker's flarelink_config cache TTL flushes on its own.
Clear credentials revokes the underlying CF token too when the keypair is Flarelink-minted. flarelink.storage.* calls will start returning R2_NOT_CONFIGURED until you regenerate or paste a new one.
Editing email templates
Flarelink ships sensible defaults for the three email-driven flows (password reset, email verification, magic link). Customize any of them from your project's Authentication - Email panel.
Three rows, one per template type: password reset · email verification · magic link. Click edit on any row.
The editor is side-by-side — subject + HTML + plain-text on the left, a live preview iframe on the right. What you see is what your users will see (the iframe runs the same substitution the Worker does).
Subject, HTML, and plain-text overrides save independently — leave a field blank to keep the default.
Available placeholders: {{url}} (the reset / verify / sign-in link) and {{appName}} (derived from your first trusted origin's hostname).
Revert to default in the editor restores the bundled template (not saved until you click Save — recoverable mid-edit).
Overrides live in your project's flarelink_config on your own D1 — Flarelink stores zero email content. The Worker picks up changes within a second.
Want to see what an email looks like end-to-end? Send a real one via the send test form right above the template list — pick the template, paste an inbox, see the rendering in Gmail / Outlook / Apple Mail before going live.
Framework quickstarts
Paste-able starters. Same SDK, same calls — just file paths and env conventions change. Each example assumes you've set FLARELINK_AUTH_URL and FLARELINK_SERVICE_KEY per Install & configure.
Next.js (App Router)
Split into two files. The server one imports 'server-only' so a stray import from a client component fails the build, not in prod.
A pure SPA can only call flarelink.auth.*. Database and storage need the service key — put those behind your own API (a small Worker, a Vercel function, etc.) and have the SPA call that.
Service-key calls only run in .astro frontmatter or src/pages/api/* endpoints. Client islands either talk to your endpoints or import a browser-only client.
In a Worker, create the client per-request from env bindings — globals don't persist across isolates the way they would on Node, and you want the request's cookie forwarded to the auth Worker anyway.