δΈζη | English | π Roadmap
π§ Note: This project is currently under intensive development and modification
This is a free, open-source, production-ready full-stack SaaS starter kit designed to help you launch your next project at unprecedented speed. It integrates modern web development tools and practices to provide you with a solid foundation.
It is also an agent-friendly SaaS template: humans use browser sessions, scripts and coding agents use API keys, and local tools can sign in through a browser-approved CLI device flow.
This starter kit provides a comprehensive set of powerful features to help you quickly build full-featured SaaS applications:
- Authentication (Better-Auth + Resend): Integrated with Better-Auth, providing secure magic link login and third-party OAuth functionality. Uses Resend for reliable email delivery with Mailchecker integration to avoid temporary emails.
- Machine Auth for APIs and Agents: Includes per-user API keys, CLI access tokens, refresh-token rotation, and a versioned
/api/v1/*surface for machine clients. - Modern Web Framework (Next.js 16 + TypeScript): Built on the latest Next.js 16 with App Router and Server Components. The entire project uses strict TypeScript type checking.
- Internationalization (Lingo.dev Compiler): Built-in localization workflow powered by
@lingo.dev/compilerfor App Router. Seedocs/i18n-lingo-migration.md. - Database & ORM (Drizzle + PostgreSQL): Uses Drizzle ORM for type-safe database operations with deep PostgreSQL integration. Supports schema migrations and optimized queries.
- Payments & Subscriptions (Creem): Integrated with Creem as the payment provider for easy subscription and one-time payment handling.
- UI Component Library (shadcn/ui + Tailwind CSS): Built with shadcn/ui, an accessible, composable component library based on Radix UI and Tailwind CSS with built-in theme support.
- Form Handling (Zod + React Hook Form): Powerful, type-safe form validation through Zod and React Hook Form.
- File Upload (Cloudflare R2): Secure file upload system based on Cloudflare R2, supporting client-side direct upload with various file type and size restrictions.
- Blog System (Content Collections): Uses Content Collections with plain Markdown files for type-safe blog content, metadata generation, and sitemap output.
- Agent-Friendly Developer Workflow: Ships with a first-party
saas-cli, browser-approved device login, API key management, and a dedicated Developer Access workspace for reviewing authorized CLI sessions. - Code Quality & Verification: Built-in ESLint, Prettier, Jest, and Playwright smoke tests to keep critical flows from regressing.
If you like this project and want to support my work, consider buying me a coffee! β
| Category | Technology |
|---|---|
| Framework | Next.js 16 |
| Language | TypeScript |
| UI | React, shadcn/ui, Tailwind v4, Lucide React (icons) |
| Auth | Better-Auth |
| Database | PostgreSQL |
| ORM | Drizzle ORM |
| Payments | Creem |
| Resend, React Email | |
| Forms | React Hook Form, Zod |
| Deployment | Vercel |
| Package Manager | pnpm |
Ensure you have the following software installed in your development environment:
# Clone the repository
git clone https://github.com/ullrai/saas-starter.git
# Enter project directory
cd saas-starter
# Install dependencies with pnpm
pnpm installThe project is configured through environment variables. First, copy the example file:
cp .env.example .envThen edit the .env file and fill in all required values.
| Variable Name | Description | Example |
|---|---|---|
DATABASE_URL |
Required. PostgreSQL connection string. | postgresql://user:password@localhost:5432/db_name |
NEXT_PUBLIC_APP_URL |
Required. Public URL of your deployed app. | http://localhost:3000 or https://yourdomain.com |
BETTER_AUTH_SECRET |
Required. Key for encrypting sessions, must be 32 characters. | a_very_secure_random_32_char_string |
RESEND_API_KEY |
Required. Resend API Key for sending emails. | re_xxxxxxxxxxxxxxxx |
CREEM_API_KEY |
Required. Creem API Key. | your_creem_api_key |
CREEM_ENVIRONMENT |
Required. Creem environment mode. | test_mode or live_mode |
CREEM_WEBHOOK_SECRET |
Required. Creem webhook secret. | whsec_your_webhook_secret |
R2_ENDPOINT |
Required. Cloudflare R2 API endpoint. | https://<ACCOUNT_ID>.r2.cloudflarestorage.com |
R2_ACCESS_KEY_ID |
Required. R2 access key ID. | your_r2_access_key_id |
R2_SECRET_ACCESS_KEY |
Required. R2 secret access key. | your_r2_secret_access_key |
R2_BUCKET_NAME |
Required. R2 bucket name. | your_r2_bucket_name |
R2_PUBLIC_URL |
Required. Public access URL for R2 bucket. | https://your-bucket.your-account.r2.dev |
GITHUB_CLIENT_ID |
Optional. GitHub OAuth Client ID. | your_github_client_id |
GITHUB_CLIENT_SECRET |
Optional. GitHub OAuth Client Secret. | your_github_client_secret |
GOOGLE_CLIENT_ID |
Optional. Google OAuth Client ID. | your_google_client_id |
GOOGLE_CLIENT_SECRET |
Optional. Google OAuth Client Secret. | your_google_client_secret |
LINKEDIN_CLIENT_ID |
Optional. LinkedIn OAuth Client ID. | your_linkedin_client_id |
LINKEDIN_CLIENT_SECRET |
Optional. LinkedIn OAuth Client Secret. | your_linkedin_client_secret |
Tip: You can generate a secure key using the following command:
openssl rand -base64 32Optional local CLI auth: for scripts, local agents, or quick terminal access, you can export
SAAS_CLI_API_KEY=ssk_...instead of storing credentials in the CLI config.
This project uses a single Drizzle config file, src/database/config.ts, and a single committed migration history in src/database/migrations/. The target database is selected only by DATABASE_URL.
For fast local iteration against your own database:
pnpm db:pushIf the schema change should be reviewed, committed, or shared with other environments, create and apply a real migration instead:
pnpm db:generate
pnpm db:migrateShared environments should use committed SQL migrations only:
# 1. Generate and commit the migration from your schema change
pnpm db:generate
# 2. Deploy the code that includes the new migration files
# 3. Run migrations once against the target DATABASE_URL
pnpm db:migrateRecommended release practice
- Never use
pnpm db:pushin staging or production.- Keep one migration history for all environments. Do not split migrations into dev/prod trees.
- Run
pnpm db:migrateas a dedicated one-shot release step in CI/CD or your deploy platform.- Do not run migrations on every application process startup.
- Make schema changes backward-compatible when possible, so app rollout and migration timing stay safe.
The project uses Content Collections plus plain Markdown files for blog content. Posts live in locale-scoped paths such as content/blog/en/*.md and content/blog/zh-Hans/*.md, authors live in content/authors/*.json, and build-time generation produces typed collections for the blog pages and sitemap.
- Authoring workflow: Add or edit posts directly in the repository with frontmatter and Markdown content.
- Generated content data: Run
pnpm content:buildto refresh the generated collections manually. The command is already wired into the build, test, and type-check scripts. - Production behavior: There is no CMS admin route or runtime content API. All blog content is built from the repository content files.
This starter distinguishes clearly between human auth and machine auth:
- Browser users: Better Auth session cookies for the web app
- Server-to-server and agent access: user-managed API keys
- Local developer tools: browser-approved device login via
saas-cli
What ships today:
- versioned machine endpoints under
/api/v1/* - API key creation and revocation in Dashboard Settings
- CLI session review and revocation in Dashboard Settings
- a terminal workflow for signing in from local tools without reusing browser session tokens
Quick examples:
# Sign in a local CLI through the browser
pnpm saas-cli -- auth login --base-url http://localhost:3000
# Check current CLI auth state
pnpm saas-cli -- auth status --base-url http://localhost:3000
# Use an API key for scripts or coding agents
SAAS_CLI_API_KEY=ssk_your_key_here pnpm saas-cli -- auth status --base-url http://localhost:3000The web app exposes management surfaces at /dashboard/developer for both API keys and authorized CLI sessions.
pnpm devNow your application should be running at http://localhost:3000!
For security reasons, the first registered user is not promoted automatically. Use the admin script after the user has signed up normally:
pnpm set:admin [email protected]The command loads .env if it exists and otherwise uses the current process environment, so the same command works locally and on a server.
After successful execution, the user receives super_admin privileges and can access /dashboard/admin.
Security tips
- Grant this role only to trusted users.
- Run the command in a secure environment with the correct
DATABASE_URL.
| Script | Description |
|---|---|
pnpm dev |
Start development server. |
pnpm build |
Build application for production. |
pnpm start |
Start production server. |
pnpm saas-cli |
Run the first-party CLI for device login and API verification. |
pnpm lint |
Check code for linting errors. |
pnpm type-check |
Run TypeScript type checking. |
pnpm test |
Run unit tests and generate coverage report. |
pnpm test:e2e |
Build and run Playwright E2E smoke tests. |
pnpm prettier:format |
Format all code using Prettier. |
pnpm set:admin |
Promote specified email user to super admin. |
This repository includes a Playwright smoke test suite in e2e/ for the most important browser-level flows:
- unauthenticated dashboard redirect
- authenticated dashboard access
- admin permission gating
- locale canonicalization for marketing routes
- API key creation and machine-auth verification
- browser-approved device auth for CLI sign-in
- Use
ShellContainerfor the marketing header, footer, and other truly wide layouts. - Use
SectionContainerfor standard marketing sections and non-dashboard page bodies. - Use
ReadingContainerfor blog articles, legal pages, and other long-form reading surfaces. - Use
CompactContainerfor auth flows. - Use
FocusContainerfor payment status and other centered cards that need more space. - Keep full-bleed backgrounds separate from content width. Backgrounds can span the viewport while content stays inside one semantic container.
Run the suite with:
pnpm test:e2eThe Playwright runner starts the production server through pnpm start and enables a test-only session route with E2E_TEST_MODE=true. That route is guarded by E2E_TEST_SECRET and should only be enabled from the Playwright runner or CI.
| Script | Description |
|---|---|
pnpm analyze |
Build application and generate bundle analysis report. |
pnpm analyze:dev |
Enable bundle analysis in development mode. |
| Script | Description |
|---|---|
pnpm db:generate |
Generate SQL migration files from schema changes. |
pnpm db:migrate |
Apply committed migrations to the database selected by DATABASE_URL. |
pnpm db:push |
Local development only. Sync schema directly without creating migration |
This project integrates a secure file upload system based on Cloudflare R2.
- Create R2 Bucket: Log into Cloudflare Dashboard, navigate to R2 and create a new bucket.
- Get API Token: In the R2 overview page, click "Manage R2 API Tokens", create a token with "Object Read & Write" permissions. Note down the
Access Key IDandSecret Access Key. - Set Environment Variables: Fill your R2 credentials and information into the
.envfile. - Configure CORS Policy: To allow browsers to upload files directly, you need to configure CORS policy in your R2 bucket's "Settings". Add the following configuration, replacing the URLs in
AllowedOriginswith your own:
[
{
"AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
"AllowedMethods": ["PUT", "GET"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]We provide a powerful FileUploader component that supports drag-and-drop, progress display, image compression, and error handling.
import { FileUploader } from "@/components/ui/file-uploader";
function MyComponent() {
const handleUploadComplete = (files) => {
console.log("Upload complete:", files);
// Handle uploaded file information here
};
return (
<FileUploader
acceptedFileTypes={["image/png", "image/jpeg", "application/pdf"]}
maxFileSize={5 * 1024 * 1024} // 5MB
maxFiles={3}
onUploadComplete={handleUploadComplete}
/>
);
}Note: This project uses a
srcdirectory structure. All components and library files are located in thesrc/directory and can be accessed through the@/path mapping which resolves tosrc/.
The component includes built-in client-side image compression functionality that can reduce image file size before upload, saving bandwidth and storage space.
<FileUploader
acceptedFileTypes={["image/png", "image/jpeg", "image/webp"]}
enableImageCompression={true}
imageCompressionQuality={0.7} // Compression quality (0.1-1.0)
imageCompressionMaxWidth={1200} // Maximum width after compression
/>This project integrates @next/bundle-analyzer to help you analyze and optimize your application's bundle size.
# Analyze production build
pnpm analyze
# Analyze in development mode
pnpm analyze:devAfter execution, bundle size analysis reports for both client and server will automatically open in your browser.
- Dynamic Imports: Use
next/dynamicfor code splitting of large components or libraries that aren't needed on first screen. - Dependency Optimization:
- Tree Shaking: Ensure you only import what you need from libraries, e.g.,
import { debounce } from 'lodash-es';instead ofimport _ from 'lodash';. - Lightweight Alternatives: Consider using lighter libraries, e.g., replace
moment.jswithdate-fns.
- Tree Shaking: Ensure you only import what you need from libraries, e.g.,
- Image Optimization: Prioritize using Next.js
<Image>component and enable WebP format.
We recommend using Vercel for deployment as it seamlessly integrates with Next.js.
-
Push to Git Repository: Push your code to a GitHub, GitLab, or Bitbucket repository.
-
Import Project in Vercel:
- Log into your Vercel account, click "Add New... > Project", then select your Git repository.
- Vercel will automatically detect this is a Next.js project and configure the build settings.
-
Configure Environment Variables:
- In your Vercel project's "Settings" -> "Environment Variables", add all the environment variables you defined in your
.envfile. Do not commit the.envfile to your Git repository.
- In your Vercel project's "Settings" -> "Environment Variables", add all the environment variables you defined in your
-
Configure Database Migration as a Release Step: Run
pnpm db:migrateonce in a dedicated CI/CD or platform release step using the productionDATABASE_URL. Avoid running migrations from web process startup hooks. -
Deploy! After completing the above steps, Vercel will automatically build and deploy your application every time you push to the main branch.
This project is licensed under the MIT license.
