Hands-on

Quick Start

Get the Sushi SaaS template running locally with pnpm, explore i18n routes, health checks, and MDX blogs, and learn where to configure auth, billing, and docs.

Before You Begin

This quick start assumes some baseline knowledge so you can move fast:

  • You know JavaScript (can read modern JS).
  • You’re familiar with the items listed as prerequisites in this guide (left panel, Prerequisites-Knowledge), or at least have a general idea of what they are.

Local Development

Prerequisites: Node 20+, pnpm 9+.

git clone https://github.com/PansaLegrand/saas-sushi-template.git saas-sushi-template
cd saas-sushi-template
pnpm install
pnpm dev

Open these URLs:

  • Landing: /en, /zh, /es, /fr, /ja
  • Health: /api/health (returns { status: "ok" })
  • Docs (MDX): /:locale/blogs/quick-start

Internationalization (next-intl)

  • Messages live in messages/*.json.
  • Locales are declared in src/i18n/locale.ts (set locales, defaultLocale, localePrefix).
  • Middleware is configured in src/middleware.ts and routes /:locale/....
  • Message loading is handled in src/i18n/request.ts.

To add a language:

  1. Add messages/<locale>.json
  2. Add the code to locales in src/i18n/locale.ts
  3. Restart dev

Docs via MDX (Fumadocs)

MDX docs live under content/docs/<locale>/.... Pages are available at /:locale/blogs/<slugs>.

  • The dev script generates .source/index.ts automatically.
  • MDX/Frontmatter is parsed by the Fumadocs Next plugin configured in next.config.ts.

Create a new page:

mkdir -p content/docs/en
echo "---\ntitle: My Page\n---\n\n# Hello" > content/docs/en/my-page.mdx

Visit /en/blogs/my-page.


Environment Variables

Before you run through the rest of the setup, create a .env file and copy the template values. This keeps secrets and URLs in one place and lets the app configure auth, storage, and payments.

  1. Create .env in the project root (copy from the template):
cp .env.example .env
# or open .env and paste the template below
  1. Fill the values you need (see explanations just below). You can leave optional blocks empty and return later.

  2. Restart the dev server after changes so Next.js and the tooling pick them up.

pnpm dev

Complete template (.env.example)

.env.example
# =============================================================================
# App Basics & URLs
# =============================================================================
# Public site base URL (used for canonical links, share URLs, Stripe redirects)
NEXT_PUBLIC_WEB_URL=http://localhost:3000
# Better Auth server base URL (usually same as site URL)
BETTER_AUTH_URL=http://localhost:3000
# Client-side Better Auth base (edge cases only; safe to leave as site URL)
NEXT_PUBLIC_AUTH_BASE_URL=http://localhost:3000

# App naming and UI defaults
NEXT_PUBLIC_APP_NAME=Sushi SaaS
NEXT_PUBLIC_PROJECT_NAME=sushi-saas-template
NEXT_PUBLIC_DEFAULT_THEME=system           # system | light | dark
NEXT_PUBLIC_LOCALE_DETECTION=false         # "true" to auto-detect browser locale
# Toggle auth entirely (default enabled unless set to "false")
NEXT_PUBLIC_AUTH_ENABLED=true

# Unique ID generator worker (Snowflake). Keep as 1 in single-instance dev.
SNOWFLAKE_WORKER_ID=1

# =============================================================================
# Database (PostgreSQL)
# =============================================================================
# Example: postgresql://user:password@localhost:5432/mydb
DATABASE_URL=

# =============================================================================
# Authentication (Better Auth) & Social Providers
# =============================================================================
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=

# Google OAuth (optional). Create credentials at
# https://console.cloud.google.com/apis/credentials
# Redirect URI (dev): http://localhost:3000/api/auth/callback/google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# =============================================================================
# Email (Resend)
# =============================================================================
# https://resend.com — required for password reset and onboarding emails
RESEND_API_KEY=
EMAIL_FROM="Your Name <founder@your-domain.com>" # Use a verified domain

# =============================================================================
# Payments (Stripe)
# =============================================================================
# Server secret key and webhook secret
STRIPE_PRIVATE_KEY=
STRIPE_WEBHOOK_SECRET=

# Public payment routes (can be relative or absolute)
NEXT_PUBLIC_PAY_SUCCESS_URL=/pricing
NEXT_PUBLIC_PAY_FAIL_URL=/pricing
NEXT_PUBLIC_PAY_CANCEL_URL=/pricing

# Optional: Stripe Price IDs (subscriptions)
# These are safe to expose; leave blank to fall back to inline price_data
NEXT_PUBLIC_STRIPE_PRICE_LAUNCH_MONTHLY=
NEXT_PUBLIC_STRIPE_PRICE_SCALE_MONTHLY=
NEXT_PUBLIC_STRIPE_PRICE_LAUNCH_YEARLY=
NEXT_PUBLIC_STRIPE_PRICE_SCALE_YEARLY=
# If using CNY prices, set the variants below
NEXT_PUBLIC_STRIPE_PRICE_LAUNCH_MONTHLY_CNY=
NEXT_PUBLIC_STRIPE_PRICE_SCALE_MONTHLY_CNY=
NEXT_PUBLIC_STRIPE_PRICE_LAUNCH_YEARLY_CNY=
NEXT_PUBLIC_STRIPE_PRICE_SCALE_YEARLY_CNY=

# =============================================================================
# Storage (S3-compatible: AWS S3, Cloudflare R2, MinIO)
# =============================================================================
# Provider selector: s3 | r2 | minio
STORAGE_PROVIDER=s3
# Leave endpoint empty for AWS S3; for R2 use https://<accountid>.r2.cloudflarestorage.com
STORAGE_ENDPOINT=
STORAGE_REGION=auto
STORAGE_ACCESS_KEY=
STORAGE_SECRET_KEY=
STORAGE_BUCKET=
# Path-style addressing (recommended true for R2/MinIO). Auto-enabled when endpoint is set.
S3_FORCE_PATH_STYLE=true
# Include ACL only if your bucket requires it (most R2/MinIO do not)
S3_USE_ACL=false
# Max single-file upload size (MB)
STORAGE_MAX_UPLOAD_MB=25
# Client-only hint to display max size in the uploader
NEXT_PUBLIC_UPLOAD_MAX_MB=25
# Alternative AWS-style names (optional synonyms). Prefer STORAGE_* above.
S3_ENDPOINT=
S3_REGION=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET=

# =============================================================================
# Analytics & Ads (optional)
# =============================================================================
# Google Analytics (G-XXXXXXX) — renders only in production
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=
# Google AdSense account code (e.g., ca-pub-XXXXXXXXXXXXXXXX)
NEXT_PUBLIC_GOOGLE_ADCODE=

# =============================================================================
# Demo Feature Flags
# =============================================================================
# Reservations demo (enabled by default)
NEXT_PUBLIC_FEATURE_RESERVATIONS_ENABLED=true
NEXT_PUBLIC_RESERVATIONS_AUTO_SEED_DEMO=true
# =============================================================================
# Logging
# =============================================================================
# Log level for server and edge logs: debug | info | warn | error
LOG_LEVEL=info

# =============================================================================
# Notifications (optional)
# =============================================================================
# Slack Incoming Webhook URL for ops alerts/notifications
SLACK_WEBHOOK_URL=

What Each Section Does (and When To Fill It)

  • App Basics & URLs: Set your base URLs first so links and redirects work. UI defaults are optional; NEXT_PUBLIC_AUTH_ENABLED disables auth UI if set to false. Keep SNOWFLAKE_WORKER_ID=1 for local.
  • Database: DATABASE_URL is required for Drizzle migrations and Better Auth tables.
  • Authentication & Social: BETTER_AUTH_SECRET (generate with openssl rand -base64 32); Google OAuth is optional.
  • Email (Resend): RESEND_API_KEY and EMAIL_FROM for password resets and welcome emails (use a verified domain).
  • Payments (Stripe): STRIPE_PRIVATE_KEY and STRIPE_WEBHOOK_SECRET for checkout + webhooks; NEXT_PUBLIC_PAY_* for redirects; Price IDs are optional.
  • Storage (S3/R2/MinIO): Set STORAGE_* (or S3_* synonyms) for uploads; path-style and ACL flags tune provider behavior; NEXT_PUBLIC_UPLOAD_MAX_MB is a UI hint.
  • Analytics & Ads: GA and AdSense IDs (only used in production).
  • Demo Feature Flags: Toggle the reservations demo.

Authentication (Better Auth)

  • Server config lives at src/lib/auth.ts and is exposed via /api/auth/[...all]/route.ts.
  • Client helpers live at src/lib/auth-client.ts and power /[locale]/login, /[locale]/signup, and /[locale]/me.
  • Set BETTER_AUTH_SECRET, BETTER_AUTH_URL, and NEXT_PUBLIC_AUTH_BASE_URL in your env files.

Environment example:

BETTER_AUTH_SECRET=generate-with: openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000

Troubleshooting

  • If /zh renders English, ensure localePrefix = "always" and restart.
  • If MDX says "Unknown module type", verify the Fumadocs plugin is enabled in next.config.ts and restart (it prints [MDX] types generated).
  • If docs 404, ensure the file is inside content/docs/<locale>/... and slugs match the URL.