Product Docs Pricing Changelog
Start free Sign in
Docs / Getting started / Quickstart

Quickstart

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.

// app/login.ts — browser import { createFlarelink } from "@flarelink/client" const flarelink = createFlarelink({ url: process.env.FLARELINK_AUTH_URL! }) await flarelink.auth.signIn({ email, password }) const me = await flarelink.auth.getMe() // User | null

On the server, add the service key — you get the full surface: database + storage + auth.

// app/server.ts — server import { createFlarelink } from "@flarelink/client" const flarelink = createFlarelink({ url: process.env.FLARELINK_AUTH_URL!, serviceKey: process.env.FLARELINK_SERVICE_KEY!, }) // Database — every query resolves to { rows, meta } const { rows: posts } = await flarelink .from<Post>("posts") .where({ author_id: userId }) .orderBy("created_at", "desc") .limit(20) // Storage — mint a presigned PUT, browser uploads to R2 directly const { url, signedHeaders } = await flarelink.storage .from("uploads") .createSignedUploadUrl("avatars/me.png", { contentType: "image/png" })

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 in await flarelink.auth.signUp({ email, password, name }) await flarelink.auth.signIn({ email, password }) // OAuth — redirects to provider by default await flarelink.auth.signInWithSocial("google") const { url } = await flarelink.auth.signInWithSocial("github", { noRedirect: true }) // Magic link await flarelink.auth.signInWithMagicLink("user@example.com") // Sign out await flarelink.auth.signOut() // Who's signed in? const user = await flarelink.auth.getMe() // User | null const 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 })

User & Session types

type User = { id: string email: string name: string emailVerified: boolean image: string | null createdAt: string updatedAt: string } type Session = { id: string userId: string expiresAt: string createdAt: string updatedAt: string ipAddress?: string | null userAgent?: string | null }

Database

Server-only. flarelink.from(table) returns a chainable that resolves on await. Every result has the same shape:

{ rows: T[], meta: { duration, rows_read?, rows_written?, last_row_id?, changes? }, }

Query builder

// SELECT — * is the default, .select() narrows const { rows: users } = await flarelink .from<User>("users") .select(["id", "email", "active"]) .where({ active: true }) // equality + AND, NULL → IS NULL .orderBy("created_at", "desc") .limit(20) .offset(40) // INSERT (single, multi, with RETURNING) await flarelink.from("users").insert({ email: "a@b.com", name: "A" }) const { rows: created } = await flarelink .from<User>("users") .insert([{ email: "a@b.com" }, { email: "c@d.com" }]) .returning("*") // UPDATE await flarelink.from("users").update({ active: false }).where({ id: 42 }) // DELETE await flarelink.from("users").delete().where({ id: 99 })

Raw SQL escape hatch

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 DESC LIMIT 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 API const { 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 R2 await fetch(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 objects await 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 account const 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
// Anything with a Request — Remix, SvelteKit, Hono, Astro, … const flarelink = createFlarelink({ url, serviceKey, cookies: () => request.headers.get("cookie") ?? "", })

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.

// lib/flarelink.client.ts — usable from "use client" components import { createFlarelink } from "@flarelink/client" export const flarelink = createFlarelink({ url: process.env.NEXT_PUBLIC_FLARELINK_AUTH_URL!, })
// lib/flarelink.server.ts — server actions, route handlers, RSC import "server-only" import { cookies } from "next/headers" import { createFlarelink } from "@flarelink/client" export const flarelink = createFlarelink({ url: process.env.NEXT_PUBLIC_FLARELINK_AUTH_URL!, serviceKey: process.env.FLARELINK_SERVICE_KEY!, cookies: () => cookies().toString(), })
// app/login/page.tsx "use client" import { flarelink } from "@/lib/flarelink.client" export default function LoginPage() { return <button onClick={() => flarelink.auth.signInWithSocial("google")}>Sign in</button> }
// app/api/posts/route.ts import { flarelink } from "@/lib/flarelink.server" export async function GET() { const { rows } = await flarelink .from("posts") .orderBy("created_at", "desc") .limit(20) return Response.json({ posts: rows }) }
# .env.local NEXT_PUBLIC_FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev FLARELINK_SERVICE_KEY=flarelink_sk_…

SvelteKit

SvelteKit's $env/static/private is statically guaranteed to never reach the browser bundle — perfect for the service key.

// src/lib/flarelink.ts — usable everywhere import { createFlarelink } from "@flarelink/client" import { PUBLIC_FLARELINK_AUTH_URL } from "$env/static/public" export const flarelink = createFlarelink({ url: PUBLIC_FLARELINK_AUTH_URL })
// src/lib/flarelink.server.ts — server-only (.server.ts is enforced by SvelteKit) import { createFlarelink } from "@flarelink/client" import { PUBLIC_FLARELINK_AUTH_URL } from "$env/static/public" import { FLARELINK_SERVICE_KEY } from "$env/static/private" export function flarelinkFor(event: { request: Request }) { return createFlarelink({ url: PUBLIC_FLARELINK_AUTH_URL, serviceKey: FLARELINK_SERVICE_KEY, cookies: () => event.request.headers.get("cookie") ?? "", }) }
// src/routes/+page.server.ts import { flarelinkFor } from "$lib/flarelink.server" export const load = async (event) => { const flarelink = flarelinkFor(event) const { rows: posts } = await flarelink.from("posts").limit(20) return { posts } }

Vite + React (browser-only SPA)

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.

// src/flarelink.ts import { createFlarelink } from "@flarelink/client" export const flarelink = createFlarelink({ url: import.meta.env.VITE_FLARELINK_AUTH_URL, })
// src/App.tsx import { useEffect, useState } from "react" import { flarelink } from "./flarelink" import type { User } from "@flarelink/client" export default function App() { const [me, setMe] = useState<User | null>(null) useEffect(() => { flarelink.auth.getMe().then(setMe) }, []) return <div>{me ? `Hi, ${me.name}` : "Signed out"}</div> }
# .env VITE_FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev

Remix

Server-only file is conventionally .server.ts — Remix won't bundle it for the browser.

// app/flarelink.server.ts import { createFlarelink } from "@flarelink/client" export function flarelinkFor(request: Request) { return createFlarelink({ url: process.env.FLARELINK_AUTH_URL!, serviceKey: process.env.FLARELINK_SERVICE_KEY!, cookies: () => request.headers.get("cookie") ?? "", }) }
// app/routes/posts._index.tsx import { flarelinkFor } from "~/flarelink.server" export const loader = async ({ request }) => { const flarelink = flarelinkFor(request) const { rows: posts } = await flarelink.from("posts").limit(20) return { posts } }

Astro

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.

// src/lib/flarelink.ts — server import { createFlarelink } from "@flarelink/client" export function flarelinkFor(request: Request) { return createFlarelink({ url: import.meta.env.PUBLIC_FLARELINK_AUTH_URL, serviceKey: import.meta.env.FLARELINK_SERVICE_KEY, cookies: () => request.headers.get("cookie") ?? "", }) }
--- // src/pages/index.astro import { flarelinkFor } from "../lib/flarelink" const flarelink = flarelinkFor(Astro.request) const { rows: posts } = await flarelink.from("posts").limit(20) --- <ul>{posts.map(p => <li>{p.title}</li>)}</ul>

Hono / Cloudflare Workers

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.

// src/index.ts import { Hono } from "hono" import { createFlarelink } from "@flarelink/client" type Env = { FLARELINK_AUTH_URL: string; FLARELINK_SERVICE_KEY: string } const app = new Hono<{ Bindings: Env }>() app.get("/posts", async (c) => { const flarelink = createFlarelink({ url: c.env.FLARELINK_AUTH_URL, serviceKey: c.env.FLARELINK_SERVICE_KEY, cookies: () => c.req.header("cookie") ?? "", }) const { rows: posts } = await flarelink.from("posts").limit(20) return c.json({ posts }) }) export default app
Next: explore the full feature breakdown, or check the changelog to see what shipped this week.