Hopp til innhold
PraxisLogg inn

Et Cloudflare-først B2B SaaS-startpunkt

Det vanskelige er allerede koblet sammen.

Arbeidsområder, autentisering, REST + MCP, webhooks, e-post, revisjon og administrasjon, typet ende til ende og bevist av en fungerende referanseapp. Den starter lokalt uten leverandørhemmeligheter.

  1. vp install
  2. pnpm run db:migrate:local
  3. pnpm run db:seed
  4. pnpm run dev
  • TanStack Start
  • Effect v4
  • Drizzle D1
  • Better Auth
  • shadcn/ui
  • Tailwind v4
  • Cloudflare Workers
  • Alchemy v2

Én forespørsel, sporet fra ende til ende.

Det samme oppslaget som fylte tallene over, fulgt fra curl-kallet som starter det til bindingen som lagrer det. Alle utdragene nedenfor er ekte kode fra dette repoet; bildeteksten på hvert panel viser stien. I sidestolpen lyser noden som omtales mens du leser.

Forespørselstopologi for B2B SaaS StarternettleserHTTP clientMCP-klientkøjobberwebTanStack StartapiREST + MCPbackgroundqueue consumerpackages/capabilitiesD1QueuesEmail
Klienter
nettleser, curl / SDK, MCP-klienter og køjobber
Workere
apps/web (TanStack Start), apps/api (REST + MCP), apps/background (køkonsument)
Delt lag
packages/capabilities: alle workere kaller de samme effektene
Infrastruktur
D1 (database), Queues (utgående webhooker), Email Service

HTTP client

Forespørselen

En vanlig GET med bearer-autentisering. Kroppen som vises nedenfor er den aktive oversikten siden brukte til å vise tallene, ikke en fixture: det første varselet vises i sin helhet, og resten telles uten omskriving.

REST · GET /workspaces/:slug/overview
curl -H "Authorization: Bearer bsk_live_xxx" \
  https://api.example.com/workspaces/starter-lab/overview

{
  "workspace": {
    "id": "wrk_starter",
    "slug": "starter-lab",
    "name": "Starter Lab",
    "planId": "team"
  },
  "notifications": []
}

apps/api

Kontrakten

Ruten finnes fordi kontrakten sier at den skal finnes. Sti, parametere, suksesskjema og den typede feilkanalen ligger i én deklarasjon: WORKSPACE_ERRORS er WorkspaceNotFound, Unauthorized, AuthorizationDenied, RateLimited og CapabilityUnavailable — kodet på endepunktet, ikke kastet som tekst. Bearer-sperren følger gruppen, så et søsterendepunkt kan ikke lanseres uten den.

HttpApiEndpoint · arbeidsområdegruppenpackages/api/src/index.ts
export const WorkspaceApi = HttpApiGroup.make('workspace')
  .add(
    HttpApiEndpoint.get('overview', '/workspaces/:slug/overview', {
      params: SlugParams,
      success: WorkspaceOverviewDto,
      error: WORKSPACE_ERRORS
    })
  )
  .middleware(BearerAuth)

packages/capabilities

Funksjonen, skrevet én gang

Begge overflater kaller samme effekt. Feilkanalen og tjenestekravene er en del av typen, slik at alle kallere deler ett feilvokabular og kompilatoren kontrollerer koblingen — en påstand på denne siden som ikke kan forfalskes.

Effect · oversiktsprojeksjonenpackages/capabilities/src/workspace-projections.ts
export const workspaceOverview: Effect.Effect<
  WorkspaceOverviewProjection,
  CapabilityUnavailable,
  WorkspaceContext | NotificationFeed
> = Effect.gen(function* () {
  const ctx = yield* WorkspaceContext
  const feed = yield* NotificationFeed
  const notifications = yield* feed.list
  return {
    workspace: ctx.workspace,
    notifications
  }
})
server fnapps/web/src/lib/server/demo-showcase.effects.ts
return runWorkspaceCapabilities(
  DEMO_WORKSPACE_SLUG,
  Effect.all({ overview: workspaceOverview, memberCount:})
)
REST handlerapps/api/src/handlers.ts
.handle('overview', ({ params, request }) =>
  workspaceRead(READ_OPERATIONS.overview, params, undefined, request)
)
MCP toolapps/api/src/mcp.ts
const invoke = yield* decodeOperationInput(operation, payload)
yield* requirePermission(yield* callerPrincipal(caller), operation.permission)
return yield* invoke

D1 · Queues · Email

Runtime-miljøet den lander på

Hver binding under effekten deklareres én gang i alchemy.run.ts: den samme TypeScript-beskrivelsen setter opp lokal utvikling og produksjon, så historien ender med deploy-kommandoen. pnpm run deploy.

De tre infrastrukturbindingene dette sporet ender i, én rad per node i skjemaet
bindinghva den inneholderdeklarert i
D1SQLite: skjema, migreringer og seed-raderpackages/db
QueuesWebhook-leveranser med nye forsøk og tilbakekoblingapps/background
EmailTransaksjonelle utsendinger, leverandørstyrt til den er konfigurertpackages/email

Valgfrie leverandører

Disse integrasjonene er valgfrie og forblir inaktive til de konfigureres.

Stripe

Stripe

Miljøstyrt

Sentry

Sentry

Miljøstyrt

PostHog

PostHog

Miljøstyrt

Cloudflare Email

E-post

Miljøstyrt

Turnstile

Turnstile

Miljøstyrt

Resonnementet er sjekket inn.

Dokumentasjon, FAQ og blogg er versjonert MDX i repoet og søkes fra genererte indekser, uten CMS. Bloggen forklarer hvorfor hver teknologibeslutning ble tatt, og utgivelser lages av release-please.

FAQ

Svar om fakturering, lisensiering og hvordan du tar B2B SaaS Starter i bruk i produktet ditt.

TanStack Start kjører direkte på en Cloudflare Worker uten Node-adapter, leverer filbasert ruting med sterkt typede lastere og ligger tett på det underliggende Web Fetch API-et. Det settes rent sammen med serverfunksjoner i Effect v4 og unngår et delt Edge/Node-runtime-miljø.

Fork det. Lokalt med 4 kommandoer.

MIT-lisensiert. Referanseappen kjører lokalt mot et eksempelarbeidsområde: ingen Stripe-nøkkel, OAuth-app eller e-postdomene er nødvendig.

kloning og hurtigstart

Kopiert
$ git clone
https://github.com/brandhaug/b2b-saas-starter.git
$ vp install
vp install
$ pnpm run db:migrate:local
pnpm run db:migrate:local
$ pnpm run db:seed
pnpm run db:seed
$ pnpm run dev
pnpm run dev
web
http://localhost:3071
api
pnpm -C apps/api dev
background
pnpm -C apps/background dev
providers
env-gated: nothing to configure