Create FasterCreate Faster
Business

Cloudflare Fullstack

Auth + RBAC dashboard with a documents CRUD on D1, R2 uploads, and a cron Worker — all on Cloudflare.

Presentation

A complete Cloudflare-native SaaS starter: a Next.js (OpenNext) web app and a Hono cron Worker in a Turborepo, sharing one D1 database and one R2 bucket. It ships email/password auth with role-based access control (admin / manager / user), a documents domain with tRPC CRUD and direct-binding R2 file uploads, and a scheduled Worker that purges expired documents. Everything runs on Cloudflare's edge with no always-on server and no connection-string database.

Composition

Apps:

Project addons:

Architecture

Two apps and three shared packages. Blueprint-specific files only:

apps/
├── web/                                # Next.js (OpenNext) app
│   ├── wrangler.jsonc                  # DB + STORAGE (R2) bindings, OpenNext worker
│   └── src/
│       ├── app/
│       │   ├── layout.tsx              # Root layout: AppProviders + sonner Toaster
│       │   ├── (auth)/
│       │   │   ├── layout.tsx          # Redirects signed-in users to /
│       │   │   ├── login/{page,login-form}.tsx
│       │   │   ├── signup/{page,signup-form}.tsx
│       │   │   ├── forgot-password/{page,forgot-password-form}.tsx
│       │   │   └── reset-password/{page,reset-password-form}.tsx
│       │   ├── (dashboard)/
│       │   │   ├── layout.tsx          # Session gate (getAuth) + sidebar shell
│       │   │   ├── page.tsx            # Documents list + upload (permission-gated)
│       │   │   ├── profile/            # Tabbed (vertical): account, security, sessions, preferences
│       │   │   │   ├── layout.tsx      # Vertical tab nav
│       │   │   │   ├── account/page.tsx    # Avatar (R2) + inline-edit display name
│       │   │   │   ├── security/page.tsx   # Change password
│       │   │   │   ├── sessions/page.tsx   # Active sessions + revoke
│       │   │   │   └── preferences/page.tsx # Theme
│       │   │   └── admin/
│       │   │       ├── layout.tsx      # Admin-only server gate
│       │   │       ├── users/page.tsx       # Users datatable + create dialog
│       │   │       ├── users/[id]/page.tsx  # Per-user editor (inline edit, role, password, ban)
│       │   │       └── roles/page.tsx       # Roles: role × permission matrix (read-only)
│       │   └── api/
│       │       ├── documents/upload/route.ts  # Multipart → R2 STORAGE.put
│       │       └── avatar/{route,[userId]/route}.ts  # Avatar upload + serve from R2
│       ├── components/
│       │   ├── navigation/             # app-sidebar, app-header, nav-user, sidebar-links
│       │   ├── admin/                  # user-table, create-user-dialog, edit-user
│       │   ├── profile/                # avatar-upload, account/security forms, session-list, tab-nav
│       │   ├── shared/data-table-shell.tsx  # shuip datatable wrapper (search + sort)
│       │   └── can.tsx                 # <Can> permission gate
│       ├── hooks/use-permission.ts     # usePermission + useCan hooks
│       └── lib/{constants,env}.ts      # ROUTES (sidebar) + getEnv() bindings
└── cron/                               # Hono scheduled Worker
    ├── wrangler.jsonc                  # crons trigger + DB + STORAGE bindings
    └── src/index.ts                    # scheduled() → purge expired documents

packages/
├── ui/src/components/                  # shuip data-table block, search-input, inline-edit + base shadcn (registry)
├── db/src/schema.ts                    # D1/sqlite schema: auth tables + documents
├── auth/src/
│   ├── auth.ts                         # createAuth(db) factory + admin plugin
│   ├── permissions.ts                  # ac/roles statements (admin/manager/user)
│   ├── email.ts                        # reset-password email stub (TODO: wire provider)
│   └── auth-client.ts                  # authClient with adminClient
└── api/src/
    ├── root.ts                         # tRPC root router
    ├── router/documents.ts             # documents list/delete
    ├── router/users.ts                 # admin user CRUD (create/update/role/password/ban)
    └── middleware/rbac.ts              # permissionProcedure / adminProcedure

scripts/seed.ts                         # Local-D1 seed (users + demo documents)
docs/agents/{auth-rbac,data-layer,storage,cloudflare-deploy}.md

The structural d1 seam (apps/web/src/lib/server.ts with getDb()/getAuth()), next.config.ts, and the base OpenNext deploy scripts come from the Next.js stack and the Cloudflare deployment — they are not blueprint files.

What's included

D1 per-request, never a singleton

D1 is only reachable through a request- or event-bound binding, so the blueprint never imports a module-level db or auth. The web app builds them per request via getDb()/getAuth() (from @/lib/server), tRPC procedures use ctx.db, and the cron Worker calls createDb(env.DB) inside scheduled(). See Data layer and the generated docs/agents/data-layer.md.

Auth & RBAC

Better Auth with the admin plugin + access control. packages/auth/src/permissions.ts is the single source of truth: a document resource and three roles — admin (full access + user management), user (own-document CRUD), manager (read-only). Server endpoints are gated with permissionProcedure('document', action) / adminProcedure; the UI gates with <Can permissions={...}> backed by usePermission. Because ac/roles are shared by the server auth and the client authClient, both stay in sync. The admin Roles page (/admin/roles) renders the role × permission matrix straight from roleDefinitions — read-only, since roles live in code. Password reset is wired too: sendResetPassword logs the link to the console in dev and calls an email.ts stub (with a TODO) in prod, behind /forgot-password and /reset-password pages. Details in the generated docs/agents/auth-rbac.md.

User management

The admin Users area is backed by a users tRPC router (all adminProcedure). /admin/users lists accounts in the shuip DataTableShell (search + sort) with a Create user dialog — the server generates a random password and reveals it once. /admin/users/[id] is a per-user editor where the display name, first/last name and phone are inline-edit fields, alongside role, password reset, ban/unban and delete. Credential- and session-touching actions go through the Better Auth admin API per-request (createAuth(ctx.db).api.* with the request headers); profile columns are written directly via Drizzle.

Dashboard shell

A collapsible shadcn sidebar (SidebarProvider + AppSidebar) drives navigation from a single ROUTES table (lib/constants.ts), permission-gated per item — a whole category label is hidden when none of its links are permitted (useCan). The footer user menu (nav-user) carries the avatar, theme toggle, and sign-out; the header renders breadcrumbs (so pages don't repeat their own title). The shadcn primitives the shell needs (sidebar, sheet, skeleton, dropdown-menu, breadcrumb, avatar, plus table, badge, dialog, popover, command, search-input) and the shuip data-table block live in packages/ui as registry-faithful code.

Forms

Every form is built on the shuip TanStack-Form field kit (useAppForm in packages/ui/src/lib/form.ts) and validates on submit. Creation and security forms use plain inputs (InputField, PasswordField, SelectField); single-value live edits use InlineEditField (the profile display name and the per-user editor commit on blur/enter).

Profile

A tabbed area (/profile) with a vertical tab rail, over authClient — no extra tRPC routers: account (avatar + inline-edit display name), security (change password), sessions (list + revoke), preferences (theme). The avatar uploads to R2 via POST /api/avatar (avatars/<userId>), is served back through GET /api/avatar/[userId] (session-gated), and the returned URL is saved to user.image with authClient.updateUser.

Documents CRUD + R2 uploads

The dashboard lists documents (own rows for users; all rows for admin/manager) and uploads files. Upload is a Route Handler (api/documents/upload) that authenticates, caps the file at 25 MB (Worker memory), STORAGE.puts it under documents/<userId>/<id>, and inserts the row. Deletion (tRPC documents.delete) removes the R2 object and the row together. See docs/agents/storage.md.

Cron Worker

apps/cron is a Hono Worker whose scheduled() handler (daily at 03:00 UTC) deletes documents whose expiresAt has passed — both the R2 object and the D1 row. Fire it locally with wrangler dev --test-scheduled.

Local seed

scripts/seed.ts opens the local miniflare D1 sqlite directly (bun:sqlite) and creates admin@example.com, manager@example.com, and user@example.com (password password), plus demo documents — a third pre-expired to exercise the cron purge. It runs automatically as part of local-setup.

Local setup & deploy

bun install
bun run local-setup     # .env files, reset + migrate local D1, generate CF types, seed
cd apps/web && bun run preview   # OpenNext preview against local bindings

Provisioning the live D1 database + R2 bucket, pasting the database_id into both wrangler.jsonc files, setting secrets, and deploying both Workers are documented in the generated docs/agents/cloudflare-deploy.md. See Cloudflare for the deployment details.

Extra dependencies

PackagePurpose
lucide-reactIcons (upload, delete, nav)
sonnerToast notifications
react-error-boundaryError boundaries
zodInput validation (tRPC, forms)
@faker-js/fakerSeed data (dev)
vaulDrawer primitive (UI package)
@tanstack/react-tableUsers datatable (UI package)
@dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilitiesDatatable row drag-reorder (UI package)
cmdkCommand palette for datatable filters (UI package)

Environment variables

VariableDescription
NEXT_PUBLIC_APP_URLPublic web app URL (scope: app)

There is no database connection string — D1 is the DB binding only.

CLI usage

bunx create-faster my-app \
  --blueprint cloudflare-fullstack \
  --linter biome \
  --git \
  --pm bun

Agent context

This blueprint ships AGENTS.md + docs/agents/ guides (auth & RBAC, data layer, storage, Cloudflare deploy) so AI coding agents understand the project out of the box. See Agent Context.

On this page