What's inside SaaScaffold
A tour of the modules, the stack and the local-first workflow behind SaaScaffold, and an honest account of what is finished.
One site, one database, one ledger
SaaScaffold is a starter for a single SaaS website. Every site built with it owns its users, orders, credits, tasks and files. There is no central SaaScaffold service in the middle, and two sites never share business data.
Inside a site, the modules share one database. That matters more than it sounds: when a payment is confirmed, the order, the payment record and the credits it grants are written in the same transaction. When an AI task finishes, the saved result, the task status and the credit deduction commit together. Splitting those steps across services or page requests is how double grants and lost credits happen.
The stack
The application runs on Cloudflare Workers and is built with TanStack Start, React and TypeScript. Files live in Cloudflare R2, transactional email goes through Resend, and Cloudflare Turnstile is available for sign-in and generation forms.
The data layer uses Drizzle ORM with two supported engines: Cloudflare D1 and PostgreSQL. Some builders want everything on Cloudflare; others already run PostgreSQL and prefer to keep it. The same business rules are tested against both engines, so choosing one is a deployment decision rather than a rewrite.
The modules
- Authentication. Email and password with verification and reset, Google sign-in, and explicit linking between sign-in methods. Accounts with the same email are never merged automatically.
- Payments and orders. Orders and purchase snapshots belong to the site, not to a payment platform. The codebase includes adapters for Stripe, PayPal and Waffo. Credits are granted only after the server verifies a payment, never because a customer landed on a success page.
- Credit ledger. Credits are issued in batches with their own expiry. Every change is an appended entry: grants, freezes, deductions, releases, expiry and audited admin adjustments.
- File storage. Uploads, downloads and generated results are stored in R2, with separate public and private buckets. Private files are streamed only after an authorization check on every request.
- AI generation. Five tools ship first: text, text-to-image, video from a prompt or a first frame, text-to-speech and instrumental music. Adapters exist for APIMart, SeeAPI and Kie.ai. In the admin center you choose a provider per generation type and enable models per tool.
- User center. Customers see their tasks with inputs and results, their files, every credit movement, their orders and their account settings.
- Admin center and notifications. Operators manage users, orders, credits, tasks, files and site configuration, and receive alerts by email or Telegram.
- Subscriptions. Recurring plans that grant credits for each paid period are in progress.
Local first
A fresh checkout does not need a cloud account. The README’s default setup runs D1, queues and scheduled jobs on your own machine:
pnpm install
cp apps/web/.dev.vars.example apps/web/.dev.vars
pnpm db:migrate
pnpm dev
pnpm check
pnpm check runs type checks, unit tests, integration tests against local services and a production build. PostgreSQL tests run when you point them at a local test database.
What “done” means
We separate three states and try never to blur them. Implemented means the code exists. Tested locally means automated tests pass against local PostgreSQL and D1, with mocked third-party HTTP. Verified live means a real payment platform, a real inbox and a real AI provider have been exercised in a deployed environment.
As of today, most modules are implemented and tested locally. Live verification and subscriptions are the main work before early access. The status section on the home page is kept up to date as that changes.
Sources & further reading
- Cloudflare Workers documentation ↗Accessed
- TanStack Start overview ↗Accessed
- Drizzle ORM: connect to Cloudflare D1 ↗Accessed
- Cloudflare D1 documentation ↗Accessed
- Cloudflare R2 documentation ↗Accessed
Spotted a mistake? Let us know.