# Flarelink — full documentation Source: https://flarelink.dev/docs ======================================================================== # Docs URL: https://flarelink.dev/docs Section: Getting started ======================================================================== Auth, database, storage, and email on your own Cloudflare account. Start with the quickstart, then go deep on each module. Docs Documentation A working backend — auth, database, storage, email — running on your own Cloudflare account. Flarelink provisions it and gets out of the way; it never sits in your app's request path. New here? Run the Quickstart first (~5 min to a deployed backend). Coming from Supabase? Read Architecture: server-first by design before you write code — the flarelink.from(…) builder looks familiar but the trust model is different (no row-level security yet; database + storage are server-only). Quickstart → Connect Cloudflare, provision a project, install @flarelink/client , ship sign-in. The shortest path from zero to a deployed app. Auth Sign-up, sign-in, OAuth, magic links, password reset. Browser + server safe. Database A typed query builder over your D1, plus a raw-SQL escape hatch. Server-only. Storage Presigned R2 uploads and downloads — bytes go browser → R2 directly. OAuth setup Step-by-step Google + GitHub OAuth app setup with the exact redirect URIs. File uploads, end to end Presign on the server, upload from the browser, record the key, show it back. Error reference Every AuthError / StorageError / DatabaseError code, with causes and fixes. Cookies & Safari Why custom domains matter, what SameSite=None; Partitioned means, a per-browser troubleshooting matrix. Limits & semantics Inherited Cloudflare limits, KV eventual consistency, presign clamps, list pagination. Framework quickstarts Paste-able setup plus a "protect a route" recipe for each. Same SDK, same calls — only file paths and env conventions change. Next.js SvelteKit Vite + React Remix Astro Hono / Workers Building with an AI coding agent? Point it at llms.txt (concise map) or llms-full.txt (every page inlined). Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Quickstart URL: https://flarelink.dev/docs/quickstart Section: Getting started ======================================================================== From an empty project to a working backend — auth, database, storage — on your own Cloudflare account, in about five minutes. 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. Coming from Supabase? Flarelink is server-first : the browser can only call flarelink.auth.* ; database and storage go through your server with the service key, and there's no row-level security yet. The query builder looks like Supabase's, but the trust model is different — read Architecture: server-first by design so nothing surprises you. 1. Connect your Cloudflare Sign up at dash.flarelink.dev , then create a scoped Cloudflare API token with these permissions: Required — all account-scoped: Account → Workers Scripts: Edit Account → D1: Edit Account → Workers KV Storage: Edit Account → Workers R2 Storage: Edit Optional : User → API Tokens: Edit — lets Flarelink mint a scoped R2 keypair for you with one click. This is a token- minting permission; if you'd rather not grant it, leave it out and paste your own R2 keypair on the Files page instead. Everything else works the same. See What the token can do . Paste the token on /connect-cf . Flarelink validates it against your account before storing it AES-256-GCM encrypted. Connect succeeds with or without the optional scope. 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. Before production: attach a custom domain. The default *.workers.dev URL is cross-site to your app, so the session cookie is a third-party cookie — and Safari and iOS block those , which means sign-in silently fails for those users. Attaching auth.yourdomain.com on your app's own domain makes the cookie same-site and fixes it. workers.dev is fine for local testing; switch before you ship. See Cookies & Safari and Custom domain for auth . 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. Developing locally? See Local development for the localhost-over-HTTPS setup Safari needs. 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 ( "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 the whole loop. Next steps: Wire your framework: Next.js , SvelteKit , Remix , Astro , Vite + React , Hono . Create your own tables: Your schema & migrations . Add social login: OAuth provider setup . Reference: Auth · Database · Storage · Errors . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Architecture: server-first by design URL: https://flarelink.dev/docs/architecture Section: Getting started ======================================================================== Why the browser can only call auth.*, why database and storage are server-only, why there's no row-level security yet — and how Flarelink stays out of your app's request path. Docs / Getting started / Architecture Architecture: server-first by design Read this before you write code, especially if you're coming from Supabase. The flarelink.from(…) builder looks familiar, but the trust model is deliberately different. The one-paragraph version. The browser can call flarelink.auth.* only. Database and storage require the service key and run on your server. There is no row-level security yet — your server route is the authorization boundary, so scope every query to the signed-in user ( where: { author_id: me.id } ). If you've been leaning on Supabase RLS to let the browser query the DB directly, that pattern doesn't exist here. Two planes: control vs. data Flarelink is a control plane . The dashboard talks to Cloudflare's REST API with your token to create and configure resources — a D1 database, a KV namespace, an R2 bucket, your auth Worker — and then walks away. Everything it provisions lives on your Cloudflare account. Your app's runtime traffic is the data plane , and Flarelink is never in it. Your users hit your auth Worker (on your account) for sign-in; your app reads its own D1; browsers upload straight to your R2. If Flarelink disappeared tomorrow, your deployed app keeps running unchanged. (This is also why "leaving is a non-event" — see Trust & verification .) Control plane (Flarelink): provisioning, the table editor, the SQL console, config writes. Uses your CF API token. Data plane (your account): the auth Worker, D1, KV sessions, R2. Flarelink never proxies this. Who can call what Surface Browser Server (with service key) flarelink.auth.* ✓ yes ✓ yes (forward cookies) flarelink.from(…) / .sql\`…\` ✗ no ✓ yes flarelink.storage.* ✗ no (bytes go browser → R2 via presigned URL) ✓ yes The service key grants full DB + R2 access for the project. Constructing a client with it in the browser would leak it — so flarelink.from(…) / flarelink.storage.* throw MissingServiceKeyError if you forget the key, and you should never put the key in a client bundle. Auth is the exception: it's safe everywhere because the session cookie, not a static secret, is the credential. No row-level security (yet) Supabase lets the browser query Postgres directly because RLS policies enforce per-row access in the database. Flarelink has no equivalent today, which is exactly why database access is server-only: with no in-database policy layer, the only safe place to enforce "user A can't read user B's rows" is your own server code. The practical rule: // ✓ authorize on the server, scope to the signed-in user const me = await flarelink.auth. getMe () // cookie-derived identity if (!me) throw new Response ( "Unauthorized" , { status: 401 }) const { rows } = await flarelink . from ( "posts" ) . where ({ author_id: me.id }) // never trust an id from the client // ✗ don't: take an id from the request body and query it unscoped Browser-side queries with row-level security policies are on the roadmap; until then, treat the server route as the boundary. Each framework's protect-a-route recipe shows the pattern. Sessions live in KV The auth Worker stores sessions in KV, never D1 — auth checks happen on every request, and KV reads are sub-millisecond and cheap, whereas a D1 row-read per request adds up. One consequence: KV is eventually consistent, so a just-signed-out session can validate for a short window at a far edge. See Limits & semantics for the details. What this buys you No lock-in. Everything runs on your Cloudflare account at Cloudflare's prices. Flarelink takes no cut and isn't a dependency at runtime. No data-processor relationship. Your users' data never touches Flarelink's servers — it's in your D1 and your R2. Verifiable auth. The auth Worker is source-available and you can confirm the deployed bundle byte-for-byte — see Trust & verification . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Local development URL: https://flarelink.dev/docs/local-development Section: Getting started ======================================================================== The honest local-dev story: a dev project, a localhost trusted origin, HTTPS-over-localhost for Safari, current limitations, and what's planned. Docs / Getting started / Local development Local development Your app runs on localhost while the auth Worker, D1, KV, and R2 it talks to are real Cloudflare resources on your account. There's no local mock of the backend yet — you develop against a live (but isolated) project. Here's the honest current setup, the sharp edges, and what's coming. Use a separate dev project Provision a second project (e.g. myapp-dev ) and point your local app at its FLARELINK_AUTH_URL and FLARELINK_SERVICE_KEY . Keep it separate from production so local experiments — schema changes, test signups, deleting rows — never touch real users. Each project is its own D1 + KV + auth Worker, so they're fully isolated. Add your localhost as a trusted origin The auth Worker rejects any request whose Origin isn't in its trusted-origins list (403 MISSING_OR_NULL_ORIGIN ). Add your local origin — the exact scheme + host + port — in the dev project's Auth panel: # whatever your dev server actually serves on: https://localhost:5173 http://localhost:3000 Add every port you use. http://localhost:3000 and https://localhost:5173 are different origins — list each one you develop on. Serve localhost over HTTPS (Safari needs it) The session cookie is Secure . Safari refuses to store a Secure cookie over http://localhost (Chrome makes a localhost exception; Safari doesn't), so sign-in appears to succeed but getMe() keeps returning null . The fix is to serve your dev server over HTTPS locally. With Vite: // vite.config.ts import mkcert from "vite-plugin-mkcert" export default { plugins: [ mkcert ()], // serves https://localhost:5173 with a trusted local cert } Then trust https://localhost:5173 in the Auth panel. Chrome users can usually get away with plain http://localhost , but standardising on HTTPS locally avoids a class of "works in Chrome, broken in Safari" bugs. The official starter ships this configured. Full cookie rationale: Cookies & Safari . Avoid cross-site cookies in dev too If your local app is on localhost:5173 and the auth Worker is on *.workers.dev , the session cookie is cross-site even locally — same third-party-cookie problem as production. The starter solves this by reverse-proxying /api/auth/* from the dev server to the auth Worker, so the cookie is first-party to localhost . If you're not using the starter, either proxy auth requests through your own dev server or accept that Safari local sign-in won't work until you do. Current limitations No offline / fully-local backend. You develop against a live dev project; you need network + a provisioned project. A wrangler dev / Miniflare local-mode story is planned but not shipped. Shared D1 with auth tables. Your tables live alongside user / account / verification / flarelink_config in the project's D1. Don't reuse those names — see Your schema & migrations . Email flows need email configured on the dev project (verification, magic link, reset). Use Resend with a test domain for the fastest setup. Planned First-class environments (dev/staging/prod per project) and a local-mode that runs the auth Worker + D1 under wrangler dev / Miniflare are on the roadmap. Until then, the dev-project pattern above is the supported path. If local DX is blocking you, tell us — hello@flarelink.dev . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Your schema & migrations URL: https://flarelink.dev/docs/schema Section: Getting started ======================================================================== How your own tables come to exist — the table editor, the SQL console, and a repeatable migration workflow you keep in your repo. Plus what's planned. Docs / Getting started / Your schema & migrations Your schema & migrations A fresh project's D1 has only the auth tables. Your app's tables — posts , notes , whatever — are yours to create. There are three ways, ordered from quickest-to-click to most repeatable. Shared D1. Your tables coexist with Flarelink's auth tables ( user , account , verification , flarelink_config ) in the same database. Don't reuse those names. Do foreign-key your tables to user(id) — the dashboard's Users admin cascade-deletes dependent rows when you remove a user, so use REFERENCES "user"(id) ON DELETE CASCADE . 1. The table editor (visual) In the dashboard's Database view, click New table . Add columns, pick logical types ( UUID , Boolean , JSON , DateTime , Email , URL ), set primary key / not-null / unique. Flarelink compiles these to the right SQLite physical types + DEFAULT / CHECK constraints (e.g. UUID → TEXT DEFAULT (lower(hex(randomblob(16)))) ). Best for getting started and one-off changes. 2. The SQL console (full control) The SQL editor runs arbitrary SQL against your D1 — same power as wrangler d1 execute , with multi-tab, saved snippets, and query history. Paste a CREATE TABLE and run it. D1 treats ; -separated statements as one atomic batch, so a multi-statement migration applies all-or-nothing. CREATE TABLE posts ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), author_id TEXT NOT NULL REFERENCES "user" (id) ON DELETE CASCADE , title TEXT NOT NULL , body TEXT NOT NULL DEFAULT '' , created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE INDEX idx_posts_author ON posts(author_id, created_at); 3. Repeatable migrations (keep SQL in your repo) For anything beyond a throwaway, keep your schema as numbered .sql files in your repo. There's no flarelink migrate CLI yet, but the D1 is on your Cloudflare account — so you can apply files with Wrangler directly, the standard Cloudflare D1 migration path. It supports whole files and multi-statement batches (which the SDK does not — see the note below). # wrangler.toml — bind your project's D1 (name + id are on the dashboard's # deployment card under "resources") [[d1_databases]] binding = "DB" database_name = "myapp-db" database_id = "…" # migrations/0001_posts.sql, 0002_comments.sql, … (checked into your repo) # apply one against your remote D1: npx wrangler d1 execute myapp-db --remote --file=migrations/0001_posts.sql # or use Wrangler's own migrations system (tracks what's applied): npx wrangler d1 migrations apply myapp-db --remote The SDK runs one statement per call. flarelink.sql\`…\` goes through the auth Worker's prepare().all() path, which executes a single statement — so it's great for runtime queries but not for applying multi-statement .sql files. Use Wrangler (above) or the dashboard SQL console (which runs ; -batches atomically) for schema work. Altering tables SQLite's ALTER TABLE is limited (no drop-column on older engines, no FK changes). The dashboard's Edit schema does the safe 12-step rebuild for you (create temp → copy → drop → rename, atomically) when you rename/reorder/retype/add/drop columns. For scripted migrations, do the same rebuild pattern in your .sql file. Foreign keys are supported by D1. Once your tables exist, query them with the database SDK . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Auth URL: https://flarelink.dev/docs/auth Section: SDK reference ======================================================================== The flarelink.auth.* surface — sign-up, sign-in, OAuth, magic links, password reset, verification. Browser + server safe. Docs / SDK reference / Auth Auth Browser + server safe — flarelink.auth.* never needs the service key. 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 . Surface accurate as of @flarelink/client v0.2.3. Mirrors the package README. // Sign up / sign in — both resolve to { user: User } const { user } = await flarelink.auth. signUp ({ email, password, name }) await flarelink.auth. signIn ({ email, password }) // OAuth — redirects to the provider by default await flarelink.auth. signInWithSocial ( "google" ) const { url } = await flarelink.auth. signInWithSocial ( "github" , { noRedirect: true }) // Magic link — first arg is the email string await flarelink.auth. signInWithMagicLink ( "user@example.com" ) // Sign out await flarelink.auth. signOut () // Who's signed in? const me = await flarelink.auth. getMe () // User | null const session = await flarelink.auth. getSession () // Session | null (no nested user) // Password reset (two steps) await flarelink.auth. requestPasswordReset ({ email: "user@example.com" , redirectTo: "https://myapp.com/reset" , // your reset page; ?token= is appended }) // …user clicks the email link, your page reads ?token= await flarelink.auth. resetPassword ({ token, newPassword }) // Email verification (manual trigger) await flarelink.auth. sendVerificationEmail ({ email }) getMe() returns the full User or null ; getSession() returns only the Session row (with userId / expiresAt ) — no nested user. Reach for getMe() when you need the user's email or name. requestPasswordReset / resetPassword / sendVerificationEmail all resolve to { status: boolean } . 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 } OAuth & magic links signInWithSocial(provider, opts?) redirects to the provider by default. Pass { noRedirect: true } to get { url } back instead — useful for SSR or when you want to render the link yourself. Providers are "google" and "github" ; configure them per OAuth provider setup . Magic-link and OAuth both depend on the email module being configured. Next Call getMe() from a server route: SSR & cookies . Gate a page on auth: Protect a route (per framework). Handle failures: Error reference . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Database URL: https://flarelink.dev/docs/database Section: SDK reference ======================================================================== flarelink.from(table) — a typed, chainable query builder over your D1, plus a raw-SQL escape hatch. Server-only. Docs / SDK reference / Database Database Server-only. flarelink.from(table) returns a chainable that resolves on await . It needs the service key, so it only works where the key is safe (your server) — never the browser. 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 ( "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 ( "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 }) The builder is immutable — each method returns a fresh object, so it's safe to compose partial queries and branch off them. Awaiting is the terminal step that fires the request. 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 ` A flarelink.sql\`…\` call runs as a single D1 batch, so a multi-statement template ( BEGIN; …; COMMIT; ) is atomic. The values you interpolate are coerced for D1's bind contract: null / undefined → null, primitives pass through, objects/arrays are JSON.stringify 'd for you. Limits & gotchas Builder .where() supports equality + AND only. Anything more dynamic ( IN , ranges, OR , joins) 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 those names for your own tables — you can read them like any other ( flarelink.from('user') ), but the dashboard locks writes to them. All queries hit the single D1 bound to your auth Worker. Multi-D1 routing and browser-side queries with row-level security are deferred — see Architecture . No batch([...]) API yet; use flarelink.sql\`…\` for multi-statement transactions. Errors are typed — see Error reference . Need to create the tables you're querying? See Your schema & migrations . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Storage URL: https://flarelink.dev/docs/storage Section: SDK reference ======================================================================== flarelink.storage.* — presigned R2 uploads and downloads. Bytes go browser → R2 directly; zero bytes through Flarelink or your server. Docs / SDK reference / Storage 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. Looking for a complete walkthrough (server route → browser upload → record key → display)? See File uploads, end to end . 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 }) Send exactly signedHeaders on the PUT — adding or omitting a header (e.g. a different Content-Type ) changes the SigV4 signature and R2 returns 403 SignatureDoesNotMatch . Presigned download const { url } = await flarelink.storage . from ( "uploads" ) . createSignedDownloadUrl ( "avatars/jane.png" ) // Use it anywhere a URL works: , window.open(url), fetch(url)… Server-only operations // Delete one or more objects (sequential under the hood) 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 () Notes expiresIn is clamped to [60, 3600] seconds. The default is 300s — fine for an upload that starts right after the URL is minted. The service key grants access to any bucket on the project's R2 account — it's the trust boundary, same as a raw R2 keypair. "Attach bucket" in the dashboard is organisation, not a security boundary. Browser uploads need the bucket's CORS to allow your app's origin. Flarelink keeps R2 CORS in sync with your auth Worker's trusted origins automatically — add an origin in the Auth panel and it propagates to attached buckets. list returns at most 1000 objects per call; pass the returned nextCursor back in to page. prefixes are the pseudo-folders under your prefix. Storage failures throw StorageError — see Error reference for R2_NOT_CONFIGURED / INVALID_SERVICE_KEY and friends. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # SSR & cookies URL: https://flarelink.dev/docs/ssr Section: SDK reference ======================================================================== Forward the browser's session cookie on server-side fetches so flarelink.auth.getMe() resolves the signed-in user from a route handler or loader. Docs / SDK reference / SSR & cookies 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). If getMe() returns null on the server even though the user is signed in, you almost always forgot cookies — or the auth Worker is on a cross-site workers.dev URL and the browser never stored the cookie in the first place. The second case is a browser problem (Safari especially); see Cookies & Safari . For the full per-framework wiring, jump to the framework quickstarts: Next.js · SvelteKit · Remix · Astro · Hono . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Error reference URL: https://flarelink.dev/docs/errors Section: SDK reference ======================================================================== Every AuthError, StorageError, and DatabaseError code the SDK and auth Worker can emit — with the cause and the fix. Docs / SDK reference / Error reference Error reference Every failure throws a typed class with .status (HTTP code) and a machine-readable .code . Branch on .code , not on message strings — messages may change, codes won't. 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 } All four classes extend FlarelinkError (which extends Error ), so err instanceof FlarelinkError catches any of them. .code is string | undefined — a few low-level transport failures arrive without one. AuthError Thrown by flarelink.auth.* . The .code is BetterAuth's machine-readable code, surfaced verbatim — so additional codes beyond the common ones below can appear. Handle the ones your UI cares about and re-throw the rest. getMe() is the one exception: it swallows a 401 and returns null instead of throwing. Code Status Cause & fix INVALID_PASSWORD 401 Wrong password on sign-in. Show a generic "email or password is incorrect" (don't reveal which). USER_NOT_FOUND 401 / 404 No account for that email. Same generic message as above to avoid account enumeration. USER_ALREADY_EXISTS 422 Sign-up with an email that's taken. Offer sign-in or password reset instead. EMAIL_NOT_VERIFIED 403 Sign-in blocked because verification is required and the email isn't verified yet. Prompt the user to check their inbox — the Worker auto-resends the link on sign-in attempts. TOO_MANY_REQUESTS 429 Rate limit (per-IP) tripped. Back off and retry; surface a "try again in a moment" message. MISSING_OR_NULL_ORIGIN 403 The request's Origin isn't in the deployment's trusted origins (or is missing). Add your app's origin in the Auth panel. The most common setup error — see Trusted origins . StorageError Thrown by flarelink.storage.* . The codes are emitted by your auth Worker's storage routes (and one is client-side). Code Status Cause & fix MISSING_SERVICE_KEY 400 Client-side: you called flarelink.storage without passing serviceKey to createFlarelink . (Thrown as MissingServiceKeyError .) Construct a server-side client with the key. INVALID_SERVICE_KEY 401 The key doesn't match the deployment's stored hash. Rotated it recently? Update your env. Mistyped? Re-copy from the dashboard. SERVICE_KEY_NOT_PROVISIONED 412 No service key has been minted for this (legacy) deployment yet. Hit Redeploy on the Auth panel — it mints one and reveals it once. R2_NOT_CONFIGURED 412 R2 credentials aren't set for the project. Attach R2 / generate or paste a keypair on the Files page. See Rotating R2 credentials . DatabaseError Thrown by flarelink.from(…) and flarelink.sql\`…\` . The first group is caught client-side before the request is sent (programmer error); the second is propagated from your auth Worker's D1 routes. Client-side (validation, status 400) Code Cause & fix INVALID_IDENTIFIER A table or column name that isn't /^[A-Za-z_][A-Za-z0-9_]*$/ . Don't interpolate user input as an identifier. UNSUPPORTED_FILTER A .where() value that isn't equality (e.g. an array or object). Use flarelink.sql\`…\` for IN , ranges, OR . INVALID_LIMIT / INVALID_OFFSET A non-integer or negative .limit() / .offset() . Pass a non-negative integer. EMPTY_INSERT / EMPTY_ROW .insert() with no rows, or a row with no columns. Provide at least one column. EMPTY_PATCH .update({}) with no columns to set. Provide at least one. Server-side (propagated from D1) Code Status Cause & fix INVALID_SQL 400 Empty or malformed SQL reached the Worker. Check your flarelink.sql\`…\` template. D1_QUERY_FAILED 400 / 500 D1 rejected the query (syntax, missing table/column, constraint violation). The underlying D1 message is included — read it. INVALID_BATCH 400 A batch request was malformed (the internal /api/db/batch route). Generally not hit through the public SDK. D1_BATCH_FAILED 400 / 500 A batch hit a D1 error; the whole batch rolled back atomically. INVALID_SERVICE_KEY / SERVICE_KEY_NOT_PROVISIONED 401 / 412 Same as in StorageError — DB routes require the service key too. MissingServiceKeyError A dedicated subclass thrown immediately (code MISSING_SERVICE_KEY , status 400) when you call flarelink.storage or flarelink.from(…) / flarelink.sql\`…\` without passing serviceKey to createFlarelink . It's a fail-fast guard against accidentally shipping a server-only call to the browser. Construct a separate server-side client with the key — never expose the key client-side. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # OAuth provider setup URL: https://flarelink.dev/docs/oauth Section: Guides ======================================================================== Step-by-step Google and GitHub OAuth setup, with the exact redirect URI Flarelink expects and where to paste the client id and secret. Docs / Guides / OAuth provider setup OAuth provider setup "Sign in with Google / GitHub" needs an OAuth app you own at the provider. You create it, copy a client id + secret into Flarelink's Auth → Providers panel, and paste one redirect URI back at the provider. The credentials live in your project's flarelink_config on your own D1 — Flarelink stores zero OAuth secrets. The redirect URI. The provider must allow exactly this callback on your auth Worker: https:///api/auth/callback/google https:///api/auth/callback/github where is your *.workers.dev URL or your custom domain . The Providers panel shows you the exact URI to copy — use that; it's the source of truth. Attaching a custom domain later changes the host, so update the provider's redirect URI then too. Google Go to the Google Cloud Console → APIs & Services → Credentials . Create or pick a project. Configure the OAuth consent screen first if prompted (External, app name, support email). Add your email as a test user while in testing. Click Create Credentials → OAuth client ID → application type Web application . Under Authorized redirect URIs , add https:///api/auth/callback/google (copy it from Flarelink's Providers panel). Create it, then copy the Client ID and Client secret into Flarelink's Auth → Providers → Google and save. Test it: flarelink.auth.signInWithSocial("google") should bounce through Google and back, signed in. A redirect_uri_mismatch from Google means the URI doesn't match exactly — check scheme, host, and the /api/auth/callback/google path character-for-character. GitHub Go to GitHub → Settings → Developer settings → OAuth Apps → New OAuth App . (For an org, use the org's Developer settings.) Homepage URL : your app's URL. Authorization callback URL : https:///api/auth/callback/github . Register the app, then Generate a new client secret . Copy the Client ID and Client secret into Flarelink's Auth → Providers → GitHub and save. GitHub OAuth Apps allow exactly one callback URL. If you run separate dev and prod auth hosts, create one OAuth App per environment (or use a custom domain in both and a GitHub App if you need multiple callbacks). After setup Call it from your app: flarelink.auth.signInWithSocial("google" | "github") — see Auth . Attaching a custom domain changes — update each provider's redirect URI (Flarelink reminds you and re-lists them). Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Email URL: https://flarelink.dev/docs/email Section: Guides ======================================================================== Configure the email module (Cloudflare Email Sending or Resend) and customize the password-reset, verification, and magic-link templates. Docs / Guides / Email 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. Email is a prerequisite for password reset, email verification, and magic-link sign-in. Until it's configured, those flows throw a clear "set up email first" error. Your Resend API key lives only in your project's flarelink_config on your own D1 — Flarelink stores zero email secrets. Resend — fastest to set up: verify a domain in Resend, paste an API key and a from-address. Good free tier. Cloudflare Email Sending — native env.EMAIL.send() ; needs a verified outbound domain on your CF account (the MX/SPF/DKIM setup). Use the send test form to fire a real message and confirm rendering in Gmail / Outlook / Apple Mail before going live. 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. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # File uploads, end to end URL: https://flarelink.dev/docs/file-uploads Section: Guides ======================================================================== A complete upload flow: server mints a presigned PUT, the browser uploads to R2 with progress, the server records the key in D1, and a presigned GET shows it back. Docs / Guides / File uploads, end to end File uploads, end to end The full loop for "user uploads an avatar and sees it back." Bytes go browser → R2 directly — never through Flarelink or your server. Your server only mints short-lived presigned URLs and records the object key. Four steps: Browser asks your server for a presigned PUT. Server authorizes the user, mints the URL, returns it. Browser PUTs the file straight to R2 (with progress), then tells your server the key. Server records the key in D1; later, mints a presigned GET to display it. 1 + 2. Server: authorize & mint the presigned PUT Resolve the user first (the service key has full R2 access — your route is the authorization boundary), then namespace the key under the user's id so one user can't overwrite another's files. // POST /api/uploads/presign — server route (framework-agnostic shape) const me = await flarelink.auth. getMe () if (!me) return json ({ error: "Unauthorized" }, 401 ) const { filename, contentType } = await req. json () const key = `avatars/${me.id}/${crypto. randomUUID ()}-${filename}` // per-user prefix const { url, signedHeaders } = await flarelink.storage . from ( "uploads" ) . createSignedUploadUrl (key, { contentType, expiresIn: 600 }) return json ({ url, signedHeaders, key }) 3. Browser: PUT to R2 with a progress bar Send exactly signedHeaders — adding or dropping a header changes the SigV4 signature and R2 returns 403 SignatureDoesNotMatch . fetch can't report upload progress, so use XMLHttpRequest when you want a progress bar. // browser async function uploadAvatar (file: File, onProgress: (pct: number ) => void ) { // 1. ask our server for a presigned PUT const res = await fetch ( "/api/uploads/presign" , { method: "POST" , headers: { "content-type" : "application/json" }, body: JSON. stringify ({ filename: file.name, contentType: file.type }), }) const { url, signedHeaders, key } = await res. json () // 2. PUT straight to R2, reporting progress await new Promise < void >((resolve, reject) => { const xhr = new XMLHttpRequest () xhr. open ( "PUT" , url) for ( const [k, v] of Object. entries (signedHeaders)) xhr. setRequestHeader (k, v as string ) xhr.upload.onprogress = (e) => e.lengthComputable && onProgress (e.loaded / e.total) xhr.onload = () => (xhr.status < 300 ? resolve () : reject ( new Error (`R2 ${xhr.status}`))) xhr.onerror = () => reject ( new Error ( "upload failed (CORS?)" )) xhr. send (file) }) // 3. tell our server the upload landed await fetch ( "/api/uploads/commit" , { method: "POST" , headers: { "content-type" : "application/json" }, body: JSON. stringify ({ key }), }) } CORS. The browser→R2 PUT is cross-origin, so the bucket's CORS must allow your app's origin. Flarelink keeps R2 CORS in sync with your auth Worker's trusted origins automatically — if uploads fail the onerror path, make sure your origin is in the Auth panel's trusted origins. See Storage . 4a. Server: record the key in D1 On commit, re-authorize and verify the key belongs to this user (defense in depth — never trust a key the client supplies without checking its prefix), then store it. // POST /api/uploads/commit — server route const me = await flarelink.auth. getMe () if (!me) return json ({ error: "Unauthorized" }, 401 ) const { key } = await req. json () if (!key. startsWith (`avatars/${me.id}/`)) return json ({ error: "bad key" }, 400 ) await flarelink. from ( "avatars" ). insert ({ user_id: me.id, r2_key: key }) return json ({ ok: true }) 4b. Server: presign a GET to display it To show the file, look up the key for the user and mint a short-lived download URL. Hand the URL to the browser; drop it straight into an . // GET /api/me/avatar — server route const me = await flarelink.auth. getMe () if (!me) return json ({ error: "Unauthorized" }, 401 ) const { rows } = await flarelink . from <{ r2_key: string }>( "avatars" ) . where ({ user_id: me.id }) . orderBy ( "id" , "desc" ) . limit ( 1 ) if (!rows.length) return json ({ url: null }) const { url } = await flarelink.storage . from ( "uploads" ) . createSignedDownloadUrl (rows[ 0 ].r2_key) return json ({ url }) // browser: Download URLs expire (default 300s, clamped to [60, 3600] ), so mint them on demand rather than caching them in long-lived markup. To delete, call flarelink.storage.from("uploads").remove([key]) from a server route after the same prefix check. Reference: Storage · Limits . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Custom domain for auth URL: https://flarelink.dev/docs/custom-domain Section: Guides ======================================================================== Bind the auth Worker to auth.yourdomain.com — makes the session cookie same-site (fixes Safari) and gives you a production-grade URL. Recommended before launch. Docs / Guides / Custom domain Custom domain for auth recommended for production The auth Worker ships on {project}.workers.dev by default — fine for local testing, but cross-site to your app, so Safari and iOS users can't sign in until you put the auth Worker on your app's own domain (see Cookies & Safari for the full why). Bind it to auth.yourdomain.com before going live: Open the project's Auth panel and click Attach custom domain Pick the zone and subdomain from the dropdown — TLS cert issues automatically (no DNS dance, no redeploy) Update your OAuth providers' redirect URIs to the new hostname (Flarelink lists the exact URIs for you) Point your app's FLARELINK_AUTH_URL at the new https://auth.yourdomain.com Put the auth Worker on a subdomain of your app's own registrable domain (app on myapp.com → auth on auth.myapp.com ). That's what makes the session cookie same-site. A custom domain that's still a different registrable domain from your app doesn't fix the third-party-cookie problem. 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. The workers.dev URL keeps working as a fallback; CF doesn't tear it down when you attach a custom domain. The Worker derives its base URL from each request, so OAuth callbacks, email links, and the cookie domain all switch to the custom hostname automatically once CF routes traffic to it — no flarelink_config change, no redeploy. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Cookies & Safari URL: https://flarelink.dev/docs/cookies Section: Guides ======================================================================== Why the session cookie is SameSite=None; Secure; Partitioned, why Safari blocks it on workers.dev, the custom-domain fix, and a per-browser troubleshooting matrix. Docs / Guides / Cookies & Safari Cookies & Safari Auth is cookie-based. The session cookie the auth Worker sets is __Secure-…; Secure; SameSite=None; Partitioned . Those attributes are correct for every HTTPS topology — but two setups still break sign-in, and both are browser/transport issues, not bugs in the cookie. Here's exactly when, and the fix. What the attributes mean Secure — only sent over HTTPS. (This is why http://localhost can't store it in Safari — see below.) SameSite=None — allows the cookie to ride cross-site requests, needed when your app and the auth Worker are on different sites. Partitioned (CHIPS) — opts into partitioned third-party cookies in browsers that support it (Chrome). Safari's CHIPS support is incomplete, so it doesn't rescue the cross-site case there. Failure 1 — cross-site in production (Safari blocks it) App on myapp.com , auth Worker on myapp-auth.workers.dev . The cookie is set by a different registrable domain than your app, so it's a third-party cookie. Safari and iOS block third-party cookies by default — sign-in looks like it works, then getMe() returns null because the cookie was never stored. Chrome currently allows it via the Partitioned attribute; Safari does not. Fix: put the auth Worker on a subdomain of your app's own domain. App on myapp.com → auth on auth.myapp.com . Now the cookie is same-site (first-party) and Safari stores it. This is the recommended production setup — see Custom domain for auth . The dashboard flags at-risk deployments (a real production origin with no custom domain attached). Failure 2 — http://localhost in dev (Safari won't store Secure) Even if you make the cookie first-party with a dev proxy, it's still Secure — and Safari refuses to store a Secure cookie over http://localhost (Chrome makes a localhost exception; Safari doesn't). So local sign-in in Safari fails until you serve your dev server over HTTPS. Fix: serve localhost over HTTPS (e.g. vite-plugin-mkcert → https://localhost:5173 ) and trust that origin. Full setup in Local development . Troubleshooting matrix Setup Chrome / Edge Safari / iOS Fix App + auth on the same domain (custom domain), HTTPS works works — (recommended) App on myapp.com , auth on *.workers.dev works (Partitioned) blocked Attach a custom domain on your app's domain Dev over http://localhost (with auth proxy) works won't store Secure Serve dev over https://localhost (mkcert) Dev over https://localhost (with auth proxy) works works — Symptom & how to confirm The tell is always the same: sign-in returns success, but getMe() / getSession() returns null right after. Confirm it's a cookie-storage problem by checking the browser's Application → Cookies for the __Secure-…session cookie — if it's absent after a "successful" sign-in, the browser declined to store it, and you're in one of the two cases above. (If the cookie is present in the browser but null on the server , that's a different issue — you forgot to forward cookies; see SSR & cookies .) Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Rotating R2 credentials URL: https://flarelink.dev/docs/rotate-r2 Section: Guides ======================================================================== Rotate your R2 access keypair safely — mint-then-revoke, fanned out to every auth Worker on the project, shown once. Docs / Guides / Rotating R2 keys 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. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Limits & semantics URL: https://flarelink.dev/docs/limits Section: Guides ======================================================================== Inherited Cloudflare limits (D1 size, Workers CPU, R2), KV eventual consistency and what it means for sign-out, presigned-URL expiry clamps, and list pagination. Docs / Guides / Limits & semantics Limits & semantics Flarelink adds almost no limits of its own — your backend runs on Cloudflare, so it inherits Cloudflare's. The few behaviours worth knowing up front are below. Cloudflare's numbers change over time; always confirm against their docs for anything you're designing around. Inherited Cloudflare limits Thing Limit (verify on CF docs) Notes D1 database size up to ~10 GB per database Per-database, not per-account. Plan a sharding/archival strategy well before you approach it. D1 query result bounded response size Paginate large reads with .limit() / .offset() ; don't SELECT * a huge table in one go. Workers CPU time ~10 ms (free) / much higher (paid) Your auth Worker is light, but heavy per-request work (e.g. high-iteration password hashing) needs the paid plan — see hashing . R2 single-PUT object up to ~5 GB (single request) Larger files need multipart upload (not yet wrapped in the SDK). No egress fees on R2. KV value size up to ~25 MB per value Sessions are tiny; not a concern in practice. Authoritative source: Cloudflare's D1 , Workers , R2 , and KV limits pages. KV eventual consistency & sign-out Sessions live in KV (see Architecture ). KV is eventually consistent across Cloudflare's edge — a write (or delete) propagates globally over a short window rather than instantly everywhere. The practical implication: when a user signs out, the session is deleted from KV, but a request hitting a far edge that hasn't seen the delete yet could still validate that session for a brief period (typically up to a minute). For the vast majority of apps this is a non-issue — but if you're building something where instant global revocation matters (e.g. killing a session after a security event), don't assume sign-out is synchronous everywhere. The same window applies to brand-new sessions just after sign-in. Presigned URL expiry expiresIn on createSignedUploadUrl / createSignedDownloadUrl is clamped to [60, 3600] seconds. The default is 300s. Values outside the range are clamped, not rejected. Mint the URL right before you use it. A download URL embedded in a long-lived page will 403 once it expires — re-mint on demand from your server. Rotating R2 credentials invalidates in-flight presigned URLs signed with the old keys — see Rotating R2 credentials . Storage list pagination storage.from(bucket).list({ prefix }) returns at most 1000 objects per call (R2's ListObjectsV2 page size). When there's more, the response carries a nextCursor — pass it back in to fetch the next page: let cursor: string | undefined do { const { objects, nextCursor } = await flarelink.storage . from ( "uploads" ) . list ({ prefix: "avatars/" , cursor }) // …process objects… cursor = nextCursor } while (cursor) The database builder has no implicit row cap, but bounded D1 response sizes mean you should always paginate large reads with .limit() + .offset() rather than pulling an entire table at once. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Next.js URL: https://flarelink.dev/docs/frameworks/nextjs Section: Framework quickstarts ======================================================================== Wire @flarelink/client into a Next.js App Router app — split client/server clients, forward cookies, and protect a route. Docs / Frameworks / Next.js 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. Assumes you've set FLARELINK_AUTH_URL and FLARELINK_SERVICE_KEY per Install & configure . // 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 } Protect a route In a Server Component or route handler, resolve the user with the server client (cookies are forwarded) and redirect when there's no session. // app/dashboard/page.tsx — Server Component import { redirect } from "next/navigation" import { flarelink } from "@/lib/flarelink.server" export default async function Dashboard () { const me = await flarelink.auth. getMe () if (!me) redirect ( "/login" ) const { rows } = await flarelink . from ( "posts" ) . where ({ author_id: me.id }) // scope every query to the signed-in user . orderBy ( "created_at" , "desc" ) return < PostList posts={rows} /> } // app/api/posts/route.ts — route handler import { flarelink } from "@/lib/flarelink.server" export async function GET () { const me = await flarelink.auth. getMe () if (!me) return new Response ( "Unauthorized" , { status: 401 }) const { rows } = await flarelink. from ( "posts" ). where ({ author_id: me.id }) return Response. json ({ posts: rows }) } Always derive ownership from me.id server-side and add it to every query's where — never trust an id sent from the client. The service key has full DB access; your route handler is the authorization boundary. (Next.js middleware can't resolve the user with the SDK — do the check in the handler / page, not middleware.) # .env.local NEXT_PUBLIC_FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev FLARELINK_SERVICE_KEY=flarelink_sk_… Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # SvelteKit URL: https://flarelink.dev/docs/frameworks/sveltekit Section: Framework quickstarts ======================================================================== Wire @flarelink/client into SvelteKit — server-only service key via $env/static/private, forward cookies, and protect a route in load. Docs / Frameworks / SvelteKit 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 (auth-only) 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 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" ) ?? "" , }) } Protect a route Resolve the user in a +page.server.ts (or +layout.server.ts to guard a whole subtree) load and throw a redirect when there's no session. // src/routes/dashboard/+page.server.ts import { redirect } from "@sveltejs/kit" import { flarelinkFor } from "$lib/flarelink.server" export const load = async (event) => { const flarelink = flarelinkFor (event) const me = await flarelink.auth. getMe () if (!me) throw redirect ( 303 , "/login" ) const { rows: posts } = await flarelink . from ( "posts" ) . where ({ author_id: me.id }) return { posts, me } } Always scope queries to me.id server-side. For a whole section, do the same check in +layout.server.ts — child routes inherit the guarded layout data. # .env PUBLIC_FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev FLARELINK_SERVICE_KEY=flarelink_sk_… Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Vite + React URL: https://flarelink.dev/docs/frameworks/vite Section: Framework quickstarts ======================================================================== Wire @flarelink/client into a browser-only Vite + React SPA — auth.* in the browser, DB/storage behind your own API, and a client-side route guard. Docs / Frameworks / Vite + React 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. Never ship the service key in a Vite bundle (anything in import.meta.env that isn't VITE_ -prefixed stays server-side; even so, don't put the key in the SPA). // 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 ( null ) useEffect (() => { flarelink.auth. getMe (). then (setMe) }, []) return < div >{me ? `Hi, ${me.name}` : "Signed out" } } Protect a route In a SPA the guard is a client-side gate: call getMe() , show a loader while it resolves, then render or redirect. This is a UX guard, not a security boundary — the real authorization happens server-side in the API the SPA calls. // src/RequireAuth.tsx import { useEffect, useState } from "react" import { Navigate } from "react-router-dom" import { flarelink } from "./flarelink" import type { User } from "@flarelink/client" export function RequireAuth ({ children }: { children: React.ReactNode }) { const [state, setState] = useState < "loading" | User | null >( "loading" ) useEffect (() => { flarelink.auth. getMe (). then (setState). catch (() => setState ( null )) }, []) if (state === "loading" ) return < p >Loading… if (!state) return < Navigate to= "/login" replace /> return <>{children} } A client-side guard only hides UI. Your data API must independently call getMe() (with the cookie forwarded) and scope every query to that user — see Hono / Workers for a matching API. # .env VITE_FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Remix URL: https://flarelink.dev/docs/frameworks/remix Section: Framework quickstarts ======================================================================== Wire @flarelink/client into Remix — a .server.ts client factory, forward cookies, and protect a route in a loader. Docs / Frameworks / Remix 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" ) ?? "" , }) } Protect a route Resolve the user at the top of the loader and throw redirect(...) when there's no session. A thrown redirect short-circuits the loader, so nothing below it runs for signed-out users. // app/routes/dashboard.tsx import { redirect } from "@remix-run/node" import { flarelinkFor } from "~/flarelink.server" export const loader = async ({ request }) => { const flarelink = flarelinkFor (request) const me = await flarelink.auth. getMe () if (!me) throw redirect ( "/login" ) const { rows: posts } = await flarelink . from ( "posts" ) . where ({ author_id: me.id }) return { posts, me } } Scope every query to me.id server-side. Reuse flarelinkFor(request) in actions to authorize mutations the same way. # .env FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev FLARELINK_SERVICE_KEY=flarelink_sk_… Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Astro URL: https://flarelink.dev/docs/frameworks/astro Section: Framework quickstarts ======================================================================== Wire @flarelink/client into Astro — a server client factory, forward cookies, and protect a page in frontmatter. Docs / Frameworks / Astro 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. (Needs an SSR adapter — e.g. @astrojs/cloudflare — so frontmatter runs on the server per request.) // 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" ) ?? "" , }) } Protect a route Resolve the user in frontmatter and return Astro.redirect(...) before rendering when there's no session. --- // src/pages/dashboard.astro import { flarelinkFor } from "../lib/flarelink" const flarelink = flarelinkFor (Astro.request) const me = await flarelink.auth. getMe () if (!me) return Astro. redirect ( "/login" ) const { rows: posts } = await flarelink . from ( "posts" ) . where ({ author_id: me.id }) --- < ul >{posts. map (p => < li >{p.title})} Same rule in src/pages/api/* endpoints: call getMe() , return a 401 on null, scope queries to me.id . # .env PUBLIC_FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev FLARELINK_SERVICE_KEY=flarelink_sk_… Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Hono / Workers URL: https://flarelink.dev/docs/frameworks/hono Section: Framework quickstarts ======================================================================== Wire @flarelink/client into a Hono app on Cloudflare Workers — per-request client from env, forward cookies, and a requireUser middleware. Docs / Frameworks / Hono / Workers 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 Protect a route A requireUser middleware constructs the client, resolves the user, and stashes it on the context — protected handlers read c.var.user and scope every query to it. // src/auth.ts import { createFlarelink, type User } from "@flarelink/client" import { createMiddleware } from "hono/factory" export const requireUser = createMiddleware <{ Bindings: Env; Variables: { user: User; flarelink: ReturnType< typeof createFlarelink> } }>( async (c, next) => { const flarelink = createFlarelink ({ url: c.env.FLARELINK_AUTH_URL, serviceKey: c.env.FLARELINK_SERVICE_KEY, cookies: () => c.req. header ( "cookie" ) ?? "" , }) const me = await flarelink.auth. getMe () if (!me) return c. json ({ error: "Unauthorized" }, 401 ) c. set ( "user" , me) c. set ( "flarelink" , flarelink) await next () }) // src/index.ts app. get ( "/my/posts" , requireUser, async (c) => { const { rows } = await c.var.flarelink . from ( "posts" ) . where ({ author_id: c.var.user.id }) // scope to the signed-in user return c. json ({ posts: rows }) }) # wrangler vars / .dev.vars FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev FLARELINK_SERVICE_KEY=flarelink_sk_… Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # What the token can do URL: https://flarelink.dev/docs/token-scope Section: Security ======================================================================== Every Cloudflare API token permission Flarelink asks for, why it's needed, what it creates — and how to connect with a minimal, non-escalating token. Docs / Security / What the token can do What the token can do The Cloudflare API token is the only secret Flarelink holds (AES-256-GCM encrypted). It scopes exactly what Flarelink can touch in your account — nothing else. Here's every permission, why it's needed, and what Flarelink creates with it. Permission Why What Flarelink creates / does Account → Workers Scripts: Edit Deploy your auth Worker Uploads the source-available auth Worker to your account; attaches a custom domain when you ask. Account → D1: Edit Your database Creates a D1 database, applies the auth schema, runs the reads/writes behind the table editor and SQL console. Account → Workers KV Storage: Edit Sessions Creates a KV namespace for sessions (sessions live in KV, never D1). Account → Workers R2 Storage: Edit File storage Creates and lists R2 buckets, applies CORS so browser uploads work. User → API Tokens: Edit optional One-click R2 keys Mints a scoped, R2-only API token (your S3 access keypair). This is a token- minting permission — see the note below. Minimal-permissions setup. User → API Tokens: Edit can mint other tokens, so if you'd rather not grant it, leave it out. Connect still succeeds — Flarelink just disables one-click R2 keys. To use storage, create an R2 API token yourself ( R2 → Manage R2 API Tokens in your Cloudflare dashboard) and paste the keypair on the Files page. There's no way to mint durable S3 keys on Cloudflare without a token-minting permission, which is why this is the one scope we made optional rather than required. It stays auditable on your side. Every R2 keypair Flarelink mints appears in your Cloudflare dashboard under My Profile → API Tokens , named flarelink-r2-… . You can view or revoke it any time — revoking it doesn't touch the rest of your setup. Cloudflare's own account audit log independently records every API call made with the token — so you can verify what Flarelink did without taking our word for it. Revoke the token entirely (in Cloudflare) and Flarelink loses all access instantly. Your deployed auth Worker, D1, KV, and R2 keep running — Flarelink is a control plane, never in your app's request path. For an in-dashboard record of what Flarelink has actually done with the token, see the Activity log . For how to verify the deployed Worker matches the published source, see the Trust & verification page. Something unclear or missing? hello@flarelink.dev llms-full.txt ↗ ======================================================================== # Activity log URL: https://flarelink.dev/docs/activity-log Section: Security ======================================================================== An append-only record of every consequential change Flarelink has made in your Cloudflare account — and how to cross-check it against Cloudflare's own audit log. Docs / Security / Activity log Activity log The dashboard's Activity page is an append-only record of every consequential change Flarelink has made in your Cloudflare account with your token — provisioning a project, deploying or redeploying an auth Worker, minting / rotating / clearing R2 credentials, attaching a custom domain, creating or deleting buckets, and config writes. Each entry shows the action, a short detail, and a timestamp. Nothing on the page can be edited or deleted. It answers "what has Flarelink ever done in my account?" from inside the dashboard. It does not log routine reads (browsing tables, listing resources) — those don't change anything. Cross-check it. For a fully independent, per-API-call record, compare against Cloudflare's own account audit log — it records all token activity regardless of what Flarelink reports. The two should agree on every change Flarelink makes. Why action-boundary logging instead of per-call? D1 queries are all POST under the hood, so reads and writes look identical at the API chokepoint — and the question you actually care about is "what changed ." So Flarelink logs one row per consequential operation (matching how Cloudflare's own audit log records changes), and leans on the CF audit log for raw per-call detail. Related: What the token can do . Something unclear or missing? hello@flarelink.dev llms-full.txt ↗