# MoveFast: complete public context



> MoveFast is a production-ready Next.js SaaS starter with authentication, payments, email, database, admin, waitlist, and launch-ready product pages.



This file combines the public documentation and published blogs. Prefer each section's Source URL when citing facts.



# Getting Started

Source: https://movefa.st/docs/getting-started

Clone your variant, install dependencies, configure environment variables, and run MoveFast locally in under 5 minutes.

MoveFast ships as 18 pre-built variants. Each variant is a specific combination of database, email provider, and payment provider. You pick the one that matches your stack.

## Variant Naming

Variants follow this pattern:

```text
<database>-<email>-<payment>
```

For example:
- `mongodb-resend-stripe` = MongoDB + Resend + Stripe
- `supabase-postmark-polar` = Supabase/PostgreSQL + Postmark + Polar
- `mongodb-nodemailer-dodo` = MongoDB + Nodemailer + Dodo Payments

**Available options:**
- **Database**: `mongodb` or `supabase` (PostgreSQL)
- **Email**: `resend`, `postmark`, or `nodemailer`
- **Payment**: `stripe`, `dodo`, or `polar`

## 1. Clone Your Variant

Each variant lives on its own Git branch:

```bash
git clone -b mongodb-resend-stripe git@github.com:movefa-st/code.git my-saas
cd my-saas
```

Replace `mongodb-resend-stripe` with whichever variant you chose.

## 2. Install Dependencies

```bash
npm install
```

If you chose a **Supabase** variant, also generate the Prisma client:

```bash
npx prisma generate
```

## 3. Configure Environment Variables

Copy the example env file:

```bash
cp .env.example .env
```

Open `.env` and fill in your credentials. Every variant includes only the variables it needs. At minimum you need:

```env
# App URLs
NEXT_PUBLIC_WEBSITE_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000

# Auth
AUTH_SECRET=generate-with-openssl-rand-base64-32
AUTH_TRUST_HOST=true

# Your database credentials (depends on variant)
# Your email provider credentials (depends on variant)
# Your payment provider credentials (depends on variant)
```

Generate a secure `AUTH_SECRET`:

```bash
openssl rand -base64 32
```

See the provider-specific docs (Database, Email, Payments) for the exact variables each provider needs.

## 4. Configure Pricing

Open `src/config/pricing.config.ts` and:

1. Set your payment model (`ONE_TIME` or `SUBSCRIPTION`)
2. Set your currency code and symbol
3. Replace the placeholder product IDs with real ones from your payment provider dashboard
4. Customize plan names, prices, and features

## 5. Run the Dev Server

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser. You should see the landing page with pricing, features, and a sign-in option.

## 6. Create Your First Admin User

1. Sign in using Google OAuth or a magic link
2. Connect to your database and manually update your user's `roles` field to include `ADMIN`

For MongoDB:
```js
db.users.updateOne({ email: "you@example.com" }, { $set: { roles: ["USER", "ADMIN"] } })
```

For Supabase/PostgreSQL:
```sql
UPDATE "User" SET roles = ARRAY['USER', 'ADMIN'] WHERE email = 'you@example.com';
```

Now you can access the admin dashboard at `/admin/dashboard`.

## Available Scripts

```bash
npm run dev        # Start development server
npm run build      # Build for production
npm run start      # Start production server
npm run lint       # Run ESLint
npm run typecheck  # TypeScript type checking
npm run email:dev  # Preview email templates at localhost:3001
```

## Next Steps

- [Project Architecture](/docs/project-architecture) to understand the codebase layout
- [Authentication](/docs/authentication) to set up Google OAuth and magic links
- [Database](/docs/database) to configure your database
- [Payments](/docs/payments) to set up checkout and webhooks
- [Email](/docs/email) to configure transactional emails

# Project Architecture

Source: https://movefa.st/docs/project-architecture

Understand the folder structure, design patterns, and how services, repositories, and routes connect in MoveFast.

MoveFast uses the Next.js App Router with a clean separation between frontend pages, API routes, and backend services. This page explains how everything fits together.

## Folder Structure

```text
src/
├── app/                    # Next.js App Router (pages + API routes)
│   ├── (main)/            # Public pages (landing, pricing, blog, auth, etc.)
│   ├── admin/             # Admin dashboard (protected)
│   ├── api/               # API route handlers
│   └── layout.tsx         # Root layout with providers
├── components/            # React components
│   ├── admin/             # Admin-specific components
│   ├── auth/              # Auth components (sign-in modal, forms)
│   ├── landing/           # Landing page sections
│   ├── ui/                # shadcn/ui primitives (button, card, dialog, etc.)
│   └── ...                # Feature-specific components
├── config/                # App configuration
│   ├── pricing.config.ts  # Pricing plans, currency, payment model
│   └── platform.config.ts # Platform access mode (open vs invite-only)
├── constants/             # Static values
│   ├── env.ts             # All environment variable names
│   ├── routes.ts          # Route path constants
│   ├── website.ts         # Brand name, URLs, support email
│   └── statusCode.ts      # HTTP status code constants
├── context/               # React context providers
│   └── AppContext.tsx      # Global user state + auth helpers
├── hooks/                 # Custom React hooks
│   ├── useCheckout.ts     # Payment checkout logic
│   ├── useSubscription.ts # Subscription management
│   ├── useUserProfile.ts  # Profile CRUD
│   └── ...
├── lib/                   # Core logic
│   ├── auth.ts            # NextAuth configuration
│   ├── auth/              # Auth utilities (page-auth, api-auth)
│   ├── backend/           # Backend services (see below)
│   ├── email/             # Email client + providers
│   ├── seo/               # Metadata helpers
│   ├── axios.ts           # Axios HTTP client
│   └── types/             # TypeScript type definitions
├── types/                 # Additional type definitions
└── utils/                 # Utility functions
    ├── errorHandling/     # ApiError, error handlers
    ├── pricing.ts         # Pricing display utilities
    └── ...
```

## Backend Services Architecture

The `src/lib/backend/` folder uses a layered pattern:

```text
src/lib/backend/
├── auth/
│   ├── services/          # Business logic
│   │   ├── magicLink.service.ts
│   │   ├── platformAccess.service.ts
│   │   └── impersonation.service.ts
│   ├── repo/              # Data access (factory + implementations)
│   │   ├── magicLink.repository.ts      # Interface + factory
│   │   ├── mongo.magicLink.repository.ts
│   │   └── supabase.magicLink.repository.ts
│   └── adapters/          # Data mapping between DB and app types
├── user/
│   ├── services/
│   ├── repo/
│   └── adapters/
├── payment/
│   ├── services/
│   ├── repo/
│   └── adapters/
├── subscription/
│   ├── services/
│   ├── repo/
│   └── adapters/
├── waitingList/
│   ├── services/
│   ├── repo/
│   └── adapters/
├── invite/
│   ├── services/
│   ├── repo/
│   └── adapters/
├── payment-providers/
│   ├── providers/         # Stripe, Dodo, Polar implementations
│   └── adapters/          # Webhook event adapters
└── webhooks/
    └── webhook-coordinator.service.ts
```

## The Repository Pattern

Every domain (user, payment, subscription, etc.) follows the same structure:

1. **Interface** defines what operations are available (e.g., `IUserRepository`)
2. **Factory function** picks the right implementation based on your variant
3. **Implementations** handle the actual database calls (Mongo or Supabase)
4. **Adapters** transform database records into app-level types

```typescript
// repo/user.repository.ts (factory)
export const createUserRepository = (): IUserRepository => {
    return new MongoUserRepository(); // or SupabaseUserRepository in supabase variants
};
```

In single-provider variants, the factory is compiled to return only the selected implementation. No runtime switching happens.

## How a Request Flows

Here is a typical API request flow:

```text
Client (React) 
  → axios call to /api/payment/checkout
  → API route handler (src/app/api/payment/checkout/route.ts)
  → withAuth() middleware checks authentication + roles
  → validateBody() validates request with Zod schema
  → paymentService.initiateOneTimePayment() (business logic)
  → createPaymentProvider() factory returns Stripe/Dodo/Polar
  → provider.createOneTimePayment() calls external API
  → handleControllerResponse() returns standardized JSON
```

## API Response Format

All API routes return a consistent shape:

```json
{
  "success": true,
  "payload": { ... },
  "message": "Optional success message"
}
```

On errors:

```json
{
  "success": false,
  "error": "Error message",
  "code": "OPTIONAL_ERROR_CODE"
}
```

## Authentication Layers

MoveFast has two auth utilities depending on context:

- **`checkAuth()`** (in `src/lib/auth/page-auth.ts`) is for Server Components and pages. It redirects users if they lack access.
- **`withAuth()`** (in `src/lib/auth/api-auth.ts`) is for API routes. It returns 401/403 JSON responses if access is denied.

Both support: `PUBLIC`, `AUTHENTICATED`, role-based (`{ roles: [UserRole.ADMIN] }`), and custom predicate functions.

## Error Handling

MoveFast uses a centralized error system:

- **`ApiError`** is a custom error class with HTTP status code and optional error code
- **`handleControllerError()`** catches errors in route handlers and returns proper HTTP responses
- **`handleInternalServerError()`** is used inside services. It re-throws `ApiError` instances and wraps unexpected errors.

## Validation

All API request bodies are validated with Zod schemas before reaching business logic:

```typescript
const validatedData = await validateBody(req, CreateCheckoutSchema);
```

Schemas live in `src/lib/zod-schemas/`.

## Key Configuration Files

| File | What it controls |
|------|-----------------|
| `src/config/pricing.config.ts` | Plans, prices, currency, payment model |
| `src/config/platform.config.ts` | Access mode (open vs invite-only) |
| `src/constants/website.ts` | Brand name, support email, social URLs |
| `src/constants/env.ts` | All environment variable names |
| `src/constants/routes.ts` | Frontend route paths |
| `src/lib/api-routes.ts` | API endpoint paths for the axios client |

# Authentication

Source: https://movefa.st/docs/authentication

Set up Google OAuth, magic link sign-in, platform access modes, role-based access control, and admin impersonation.

MoveFast uses [Auth.js](https://authjs.dev) (NextAuth v5) with two sign-in methods out of the box: Google OAuth and email magic links.

## Environment Variables

```env
AUTH_SECRET=your-generated-secret
AUTH_TRUST_HOST=true
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
```

Generate `AUTH_SECRET` with:

```bash
openssl rand -base64 32
```

For production, update `NEXTAUTH_URL` to your deployed domain.

## Google OAuth Setup

1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project (or use an existing one)
3. Navigate to APIs & Services > Credentials
4. Create an OAuth 2.0 Client ID (Web application type)
5. Add authorized redirect URIs:

```text
http://localhost:3000/api/auth/callback/google
https://yourdomain.com/api/auth/callback/google
```

6. Copy the Client ID and Client Secret into your `.env`

## Magic Link Sign-In

Magic links let users sign in without a password. Here is how the flow works:

1. User enters their email on the sign-in page
2. Server generates a random token (32 bytes, 15-minute expiry)
3. Token is stored in the database
4. An email is sent with a clickable link containing the token
5. User clicks the link, which verifies the token and creates a session

Magic links require a working email provider. Make sure your email setup is complete before testing this flow.

## Platform Access Modes

MoveFast supports two access modes, configured in `src/config/platform.config.ts`:

```typescript
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.WAITLIST_WITH_INVITES;
```

### WAITLIST_WITH_INVITES (default)

- New users cannot sign up freely
- They must be invited by an admin (via the invite system)
- A waitlist form is shown on the landing page for collecting emails
- Existing users (already in the database) can always log in

This is great for pre-launch. Collect emails, then invite users in batches.

### OPEN

- Anyone can sign up without restrictions
- No waitlist form is shown
- Standard signup experience

Switch to `OPEN` when you are ready for public launch:

```typescript
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.OPEN;
```

## Role-Based Access Control

Every user has a `roles` array. The built-in roles are:

- **USER**: Default role assigned to all new users
- **ADMIN**: Can access the admin dashboard and manage users

### Protecting Pages (Server Components)

Use `checkAuth()` in page components:

```typescript
import { checkAuth } from "@/lib/auth/page-auth";
import { AccessLevel } from "@/lib/types/auth.enums";
import { UserRole } from "@/lib/types/auth.enums";

// Require any authenticated user
const user = await checkAuth({ access: AccessLevel.AUTHENTICATED });

// Require admin role
const user = await checkAuth({ access: { roles: [UserRole.ADMIN] } });
```

If the user does not meet the access requirement, they are automatically redirected.

### Protecting API Routes

Use `withAuth()` in route handlers:

```typescript
import { withAuth } from "@/lib/auth/api-auth";
import { AccessLevel } from "@/lib/types/auth.enums";

export const GET = withAuth(AccessLevel.AUTHENTICATED, async ({ req, user }) => {
    // user is guaranteed to be authenticated here
    return Response.json({ userId: user.id });
});
```

Access levels:
- `AccessLevel.PUBLIC` - no auth required, `user` can be null
- `AccessLevel.AUTHENTICATED` - must be logged in
- `{ roles: [UserRole.ADMIN] }` - must have the ADMIN role
- Custom predicate function: `(user) => user.planId !== null`

## Admin Impersonation

Admins can "impersonate" any non-admin user to see the app exactly as that user sees it. This is useful for debugging user-reported issues.

How it works:
1. Admin clicks "Impersonate" on a user in the admin dashboard
2. A signed, HttpOnly cookie is set (HMAC-SHA256, 4-hour expiry)
3. All subsequent requests resolve to the impersonated user
4. A banner appears at the top showing who is being impersonated
5. Admin clicks "Stop" to end impersonation

Security constraints:
- Only users with the ADMIN role can impersonate
- Cannot impersonate other admins
- Cannot impersonate inactive users
- Cookie is cryptographically signed to prevent tampering
- Impersonation auto-expires after 4 hours
- Signing out clears impersonation

## Adding a New OAuth Provider

Auth.js supports many providers. To add one (e.g., GitHub):

1. Install the provider if needed (most are built into Auth.js)
2. Open `src/lib/auth.ts`
3. Add the provider to the `providers` array:

```typescript
import GitHub from "next-auth/providers/github";

providers: [
    Google({ ... }),
    GitHub({
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    Credentials({ ... }),
],
```

4. Add the env variables to `src/constants/env.ts` and your `.env`
5. The `signIn` callback handles new user creation automatically

## Sign-In Page

The sign-in page lives at `/signin` and shows:
- Google sign-in button
- Magic link email form
- Context-aware messaging based on platform access mode

There is also a `SignInModal` component that can be triggered from anywhere in the app using `useAppContext().setIsLoginDialogOpen(true)`.

# Database

Source: https://movefa.st/docs/database

Set up MongoDB with Mongoose or PostgreSQL with Prisma and Supabase. Understand the schema, collections, and how to switch databases.

MoveFast supports two database options. Your variant comes pre-configured with one of them:

- **MongoDB** variants use Mongoose for schema-less document storage
- **Supabase** variants use Prisma 7 with PostgreSQL

You do not need to configure both. Your variant only includes the code for the database you selected.

---

## MongoDB Setup

### Environment Variable

```env
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/your-db-name
```

### Getting a MongoDB Database

**Option A: MongoDB Atlas (recommended for production)**
1. Create a free account at [mongodb.com/atlas](https://www.mongodb.com/cloud/atlas)
2. Create a cluster
3. Create a database user with read/write access
4. Get the connection string and add it to your `.env`
5. Whitelist your IP (or allow access from anywhere for Vercel)

**Option B: Local MongoDB**
1. Install MongoDB locally
2. Use `mongodb://localhost:27017/movefast` as your URI

### Collections

MongoDB creates collections automatically when the first document is inserted. The app uses these collections:

| Collection | Purpose |
|-----------|---------|
| `users` | User accounts with roles, status, plan info |
| `magiclinktokens` | Temporary magic link verification tokens |
| `waitinglists` | Email waitlist entries |
| `invites` | Admin-created invite records |
| `payments` | Payment transaction records |
| `subscriptions` | Subscription lifecycle records |

### How It Works

Mongoose models are defined in `src/lib/schemas/`. The connection is established lazily on first use via `src/lib/mongodb.ts`, which caches the connection across hot reloads in development.

---

## Supabase / PostgreSQL Setup

### Environment Variables

```env
DATABASE_URL=postgresql://user:password@host:5432/dbname
```

If your provider requires a direct connection (some do for migrations):

```env
DIRECT_URL=postgresql://user:password@host:5432/dbname
```

### Getting a PostgreSQL Database

**Option A: Supabase (recommended)**
1. Create a project at [supabase.com](https://supabase.com)
2. Go to Project Settings > Database
3. Copy the connection string (use the "Transaction" pooler for `DATABASE_URL`)
4. For `DIRECT_URL`, use the "Session" pooler or direct connection

**Option B: Any PostgreSQL provider**

Any PostgreSQL database works. Neon, Railway, Render, or a self-hosted instance are all fine.

### Generate Prisma Client

After installing dependencies, generate the Prisma client:

```bash
npx prisma generate
```

This creates the client in `generated/prisma/`. You must run this:
- After every fresh `npm install`
- In CI before `npm run build`

### Push Schema to Database

Apply the schema to your database:

```bash
npx prisma db push
```

This creates all the tables defined in `prisma/schema.prisma`.

### Schema Overview

The Prisma schema defines these tables:

| Table | Purpose |
|-------|---------|
| `User` | User accounts (id, email, name, roles, isActive, planId) |
| `VerificationToken` | Magic link tokens (identifier, token, expires) |
| `WaitingList` | Waitlist entries with email tracking |
| `Invite` | Invite records with token, expiry, used/revoked status |
| `Payment` | Payment records linked to users |
| `Subscription` | Subscription records with provider details |

### Importing the Prisma Client

The generated client lives at `generated/prisma/`, not in `node_modules`. Import it using the path alias:

```typescript
import type { PrismaClient } from "@/prisma/client";
```

The client is constructed lazily in `src/lib/prisma.ts` with a PostgreSQL driver adapter. This means `next build` works even without live database credentials.

### Prisma Studio

To browse your data visually:

```bash
npx prisma studio
```

---

## Data Models

Both database options store the same logical entities. Here is what each model contains:

### User
- `id` - Unique identifier
- `email` - Unique email address
- `name` - Display name
- `image` - Profile image URL (from OAuth)
- `roles` - Array of roles (`USER`, `ADMIN`)
- `isActive` - Whether the account is enabled
- `planId` - The product ID of their purchased plan (for one-time payments)
- `emailVerified` - When their email was verified
- `createdAt` / `updatedAt` - Timestamps

### Payment
- `userId` - Reference to the user
- `provider` - Which payment provider (STRIPE, DODO, POLAR)
- `providerPaymentId` - The provider's payment ID
- `status` - Payment status (COMPLETED, etc.)
- `amount` - Payment amount
- `currency` - Currency code
- `planId` - Which plan was purchased

### Subscription
- `userId` - Reference to the user
- `provider` - Which payment provider
- `providerSubscriptionId` - The provider's subscription ID
- `providerCustomerId` - The provider's customer ID
- `planId` - Which plan
- `status` - ACTIVE, CANCELLED, PAST_DUE, etc.
- `currentPeriodStart` / `currentPeriodEnd` - Billing period dates
- `cancelAt` / `cancelledAt` - Cancellation dates

---

## Tips

- Use a **separate database** for development and production to avoid polluting real data with test signups
- For MongoDB Atlas, make sure to whitelist `0.0.0.0/0` if deploying to Vercel (serverless IPs change)
- For Supabase, use the pooled connection string for `DATABASE_URL` in serverless environments
- Run `npx prisma generate` in your CI/CD build step before `npm run build`

# Payments

Source: https://movefa.st/docs/payments

Configure Stripe, Dodo Payments, or Polar for one-time purchases and subscriptions. Set up checkout, webhooks, and billing portal.

MoveFast supports three payment providers. Your variant comes with one of them pre-configured:

- **Stripe** - The most popular payment processor
- **Dodo Payments** - Merchant of record (handles taxes for you)
- **Polar** - Built for developers and open source

All three support both one-time payments and recurring subscriptions through the same unified interface.

---

## Payment Architecture

MoveFast uses a provider-agnostic design. All payment providers implement the same interface:

```typescript
interface IPaymentProvider {
    createOneTimePayment(params) → CheckoutSession
    createSubscription(params) → CheckoutSession
    cancelSubscription(subscriptionId) → void
    getSubscription(subscriptionId) → SubscriptionDetails
    handleWebhookEvent(payload, signature) → WebhookHandleResult
    createBillingPortalSession(customerId) → { url }
}
```

This means the rest of your app never talks to Stripe/Dodo/Polar directly. It always goes through the service layer.

---

## Payment Models

Configure your payment model in `src/config/pricing.config.ts`:

```typescript
export const PAYMENT_MODEL: PaymentModel = PaymentModel.ONE_TIME;
// or
export const PAYMENT_MODEL: PaymentModel = PaymentModel.SUBSCRIPTION;
```

**ONE_TIME**: Users pay once and get lifetime access. The purchased `planId` is stored on the user record.

**SUBSCRIPTION**: Users pay monthly or yearly. A subscription record is created and managed through webhooks.

---

## Configuring Pricing Plans

Edit the `PRICING_PLANS` array in `src/config/pricing.config.ts`:

```typescript
export const PRICING_PLANS: PricingPlan[] = [
    {
        name: "Free",
        description: "Perfect for trying out",
        productIds: {
            monthly: "free",   // Special value, no checkout needed
            yearly: "free",
        },
        price: { monthly: 0, yearly: 0 },
        oneTimePrice: 0,
        features: [
            { name: "Feature one", included: true },
            { name: "Feature two", included: false },
        ],
        cta: { text: "Get Started", variant: "outline" },
    },
    {
        name: "Pro",
        productIds: {
            monthly: "your-monthly-product-id",
            yearly: "your-yearly-product-id",
            oneTime: "your-one-time-product-id",
        },
        price: { monthly: 29, yearly: 23 },
        oneTimePrice: 149,
        highlighted: true,
        badge: "Most Popular",
        features: [ ... ],
        cta: { text: "Start Free Trial", variant: "default" },
    },
];
```

Key points:
- `productIds` must match the product/price IDs in your payment provider dashboard
- For `ONE_TIME` model, use the `oneTime` field in `productIds`
- For `SUBSCRIPTION` model, use the `monthly` and `yearly` fields
- `price.yearly` is the per-month price when billed annually (for display)
- Set `highlighted: true` to visually emphasize a plan
- Set `badge` to show a label like "Most Popular"

### Currency

```typescript
export const DEFAULT_CURRENCY = "USD";   // Sent to payment provider (ISO 4217)
export const CURRENCY_SYMBOL = "$";      // Shown in the UI
```

---

## Stripe Setup

### 1. Create an Account

Sign up at [stripe.com](https://stripe.com) and complete onboarding.

### 2. Get API Keys

Go to [dashboard.stripe.com/apikeys](https://dashboard.stripe.com/apikeys) and copy your secret key.

### 3. Environment Variables

```env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
```

### 4. Create Products

Go to [dashboard.stripe.com/products](https://dashboard.stripe.com/products) and create products for each plan. You do not need to create prices in Stripe since MoveFast creates them dynamically using `price_data` during checkout.

Use the product names as reference, then put any identifier in `productIds` in your pricing config (MoveFast uses these as metadata, not as Stripe price IDs).

### 5. Set Up Webhooks

In Stripe Dashboard > Developers > Webhooks, add an endpoint:

```text
https://yourdomain.com/api/payment/webhook/stripe
```

Select these events:
- `payment_intent.succeeded`
- `invoice.payment_succeeded`
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`

Copy the webhook signing secret into `STRIPE_WEBHOOK_SECRET`.

### 6. Test Mode

Use `sk_test_` keys during development. Switch to `sk_live_` for production.

---

## Dodo Payments Setup

### 1. Create an Account

Sign up at [dodopayments.com](https://dodopayments.com).

### 2. Environment Variables

```env
DODO_API_KEY=your-dodo-api-key
DODO_WEBHOOK_SECRET=your-dodo-webhook-secret
DODO_ENVIRONMENT=test_mode
```

### 3. Create Products

Create products in the Dodo dashboard. Copy each product ID into `productIds` in your pricing config.

### 4. Set Up Webhooks

Add this webhook endpoint in your Dodo dashboard:

```text
https://yourdomain.com/api/payment/webhook/dodo
```

### 5. Switch to Production

When ready for real payments, change:

```env
DODO_ENVIRONMENT=live_mode
```

---

## Polar Setup

### 1. Create an Account

Sign up at [polar.sh](https://polar.sh).

### 2. Create an Access Token

In your Polar dashboard, create an organization access token.

### 3. Environment Variables

```env
POLAR_ACCESS_TOKEN=polar_at_...
POLAR_WEBHOOK_SECRET=your-webhook-secret
POLAR_ENVIRONMENT=sandbox
```

### 4. Create Products

Create products in the Polar dashboard. Copy each product ID into `productIds` in your pricing config.

### 5. Set Up Webhooks

Add this webhook endpoint in Polar:

```text
https://yourdomain.com/api/payment/webhook/polar
```

Select these events:
- `order.paid`
- `subscription.created`
- `subscription.updated`
- `subscription.active`
- `subscription.canceled`
- `subscription.revoked`

### 6. Switch to Production

```env
POLAR_ENVIRONMENT=production
```

---

## How Checkout Works

1. User clicks a pricing plan button on the frontend
2. Frontend calls `POST /api/payment/checkout` with the `productId`
3. Server validates the request and checks if the user already has an active subscription (for subscription model)
4. Server calls the payment provider to create a checkout session
5. Server returns the checkout URL
6. Frontend redirects the user to the hosted checkout page
7. After payment, user is redirected back to `/payments`
8. The payment provider sends a webhook to confirm the payment
9. Webhook handler creates a payment/subscription record in your database

## How Webhooks Work

All three providers send webhook events to your app when something happens (payment completed, subscription cancelled, etc.). MoveFast normalizes these into 5 standard event types:

| Event | What it means |
|-------|--------------|
| `PAYMENT_COMPLETED` | One-time payment succeeded |
| `SUBSCRIPTION_CREATED` | New subscription started |
| `SUBSCRIPTION_UPDATED` | Subscription renewed or plan changed |
| `SUBSCRIPTION_CANCELLED` | User cancelled (may still be active until period end) |
| `SUBSCRIPTION_DELETED` | Subscription fully terminated |

The `WebhookCoordinator` routes each event to either `PaymentService` or `SubscriptionService` for processing.

## Billing Portal

For subscription users, MoveFast provides a "Manage Subscription" button that opens the payment provider's billing portal. Users can:
- View their subscription details
- Update payment method
- Cancel or resume subscription
- View invoice history

The billing portal URL is fetched via `GET /api/subscription/billing-portal`.

---

## Testing Webhooks Locally

To test webhooks during development, you need to expose your local server to the internet.

**For Stripe**, use the Stripe CLI:

```bash
stripe listen --forward-to localhost:3000/api/payment/webhook/stripe
```

**For Dodo and Polar**, use a tool like [ngrok](https://ngrok.com):

```bash
ngrok http 3000
```

Then add the ngrok URL as your webhook endpoint in the provider dashboard temporarily.

# Email

Source: https://movefa.st/docs/email

Configure Nodemailer, Resend, or Postmark for transactional emails. Customize templates and preview them locally.

MoveFast uses [React Email](https://react.email) for beautiful, type-safe email templates. Your variant comes with one email provider pre-configured:

- **Nodemailer** - Send via any SMTP server (Gmail, AWS SES, your own server)
- **Resend** - Modern email API built for developers
- **Postmark** - Reliable transactional email with great deliverability

---

## Nodemailer Setup (SMTP)

Nodemailer sends emails through any SMTP server. This is the most flexible option.

### Environment Variables

```env
SMTP_HOST=smtp.gmail.com
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
EMAIL_FROM_EMAIL=noreply@yourdomain.com
EMAIL_FROM_NAME=MoveFast
```

### Common SMTP Providers

**Gmail:**
- Host: `smtp.gmail.com`
- Use an [App Password](https://myaccount.google.com/apppasswords) (not your regular password)

**AWS SES:**
- Host: `email-smtp.us-east-1.amazonaws.com`
- Use IAM SMTP credentials

**Custom SMTP:**
- Use whatever host, username, and password your provider gives you

---

## Resend Setup

Resend is a modern email API with a generous free tier.

### Environment Variables

```env
RESEND_API_KEY=re_xxxxxxxxx
EMAIL_FROM_EMAIL=noreply@yourdomain.com
EMAIL_FROM_NAME=MoveFast
```

### Setup Steps

1. Create an account at [resend.com](https://resend.com)
2. Verify your sending domain (add DNS records)
3. Generate an API key
4. Set `EMAIL_FROM_EMAIL` to an address on your verified domain

---

## Postmark Setup

Postmark is known for excellent delivery rates and fast delivery times.

### Environment Variables

```env
POSTMARK_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
EMAIL_FROM_EMAIL=noreply@yourdomain.com
EMAIL_FROM_NAME=MoveFast
```

### Setup Steps

1. Create an account at [postmarkapp.com](https://postmarkapp.com)
2. Verify your sending domain or email address
3. Get your server API token from Server > API Tokens
4. Set `EMAIL_FROM_EMAIL` to your verified sender address

---

## Included Email Templates

MoveFast ships with 7 ready-to-use email templates in the `emails/` folder:

| Template | When it is sent |
|----------|----------------|
| `MagicLinkLoginEmail` | User requests a magic link to sign in |
| `AdminNewUserSignUpEmail` | A new user signs up (sent to admin) |
| `ContactFormNotificationEmail` | Someone submits the contact form (sent to admin) |
| `UserInvitationEmail` | Admin invites a user to the platform |
| `WaitingListWelcomeEmail` | User joins the waitlist |
| `WaitingListLaunchEmail` | Admin sends launch announcement to waitlist |
| `WaitingListReminderEmail` | Admin sends reminder to waitlist users |

All templates use shared components (`EmailHeader`, `EmailFooter`) for consistent branding.

---

## Preview Emails Locally

React Email includes a dev server that lets you see your templates in the browser:

```bash
npm run email:dev
```

Open [http://localhost:3001](http://localhost:3001) to browse and preview all templates with live reloading.

---

## Customizing Templates

Templates are React components in the `emails/` folder. Here is a simplified example:

```tsx
// emails/WaitingListWelcomeEmail.tsx
import { EmailHeader } from "./components/EmailHeader";
import { EmailFooter } from "./components/EmailFooter";
import { Html, Body, Text } from "@react-email/components";

interface Props {
    email: string;
}

export default function WaitingListWelcomeEmail({ email }: Props) {
    return (
        <Html>
            <Body>
                <EmailHeader />
                <Text>Thanks for joining our waitlist, {email}!</Text>
                <Text>We will let you know when we launch.</Text>
                <EmailFooter />
            </Body>
        </Html>
    );
}
```

To customize:
1. Edit the template components in `emails/`
2. Update the shared `EmailHeader` and `EmailFooter` for branding
3. Preview changes at `localhost:3001`

---

## Sending Emails in Your Code

To send an email from anywhere in the backend:

```typescript
import { sendEmailWithTemplate } from "@/lib/email/sendEmail";
import YourTemplate from "@emails/YourTemplate";

await sendEmailWithTemplate({
    template: YourTemplate({ name: "John" }),
    subject: "Your Subject Line",
    to: { email: "user@example.com" },
});
```

The `sendEmailWithTemplate` function:
1. Renders the React component to HTML using `react-email`
2. Passes it to the configured email provider
3. Returns the send result (or `null` if it fails silently)

---

## Adding a New Email Template

1. Create a new file in `emails/`:

```tsx
// emails/MyNewEmail.tsx
import { EmailHeader } from "./components/EmailHeader";
import { EmailFooter } from "./components/EmailFooter";

interface Props {
    userName: string;
    message: string;
}

export default function MyNewEmail({ userName, message }: Props) {
    return (
        // Your template JSX
    );
}
```

2. Import and use it in your service:

```typescript
import MyNewEmail from "@emails/MyNewEmail";

await sendEmailWithTemplate({
    template: MyNewEmail({ userName: "Alice", message: "Hello!" }),
    subject: "New notification",
    to: { email: "alice@example.com" },
});
```

3. Preview it with `npm run email:dev`

---

## Tips

- Always use a verified domain for `EMAIL_FROM_EMAIL`. Free email addresses (gmail.com, etc.) will get rejected by most providers.
- In development, you can use Resend's free tier (100 emails/day) or a Gmail app password with Nodemailer.
- Check spam folders if test emails are not arriving.
- The `sendEmailWithTemplate` function swallows errors by default (returns `null`). This prevents email failures from breaking user-facing flows like sign-up. Errors are still logged.

# Admin Dashboard

Source: https://movefa.st/docs/admin-dashboard

Manage users, view analytics, handle waitlist entries, send invites, and impersonate users from the built-in admin panel.

MoveFast includes a full admin dashboard at `/admin/dashboard`. It is protected by role-based access, so only users with the `ADMIN` role can access it.

## Accessing the Dashboard

After creating your first admin user (see [Getting Started](/docs/getting-started)), navigate to:

```text
http://localhost:3000/admin/dashboard
```

The dashboard has a sidebar with navigation to all admin sections.

---

## Dashboard Overview

The main dashboard page shows:

- **Total Users** - Number of registered users with growth percentage
- **Active Users** - Users with `isActive: true`
- **Inactive Users** - Users that have been disabled
- **Waitlist Count** - Number of emails on the waitlist
- **New Users Today** - Sign-ups in the current day
- **User Growth Chart** - Visual chart showing user registrations over time

The growth chart appears once you have enough users to show meaningful data.

---

## User Management

**Route:** `/admin/dashboard/users`

Here you can:

### View All Users
A table showing every registered user with their name, email, role, status, plan, and sign-up date.

### Toggle User Status
Click the toggle to enable or disable a user. Disabled users:
- Cannot log in
- Get redirected to an "Account Disabled" page if they have an existing session
- Admin users cannot be disabled (safety protection)

### Impersonate Users
Click "Impersonate" to see the app exactly as that user sees it. A banner appears at the top of the page showing you are impersonating. Click "Stop" to return to your admin session.

Rules:
- You can only impersonate non-admin users
- You cannot impersonate inactive users
- Impersonation expires after 4 hours automatically
- Signing out clears the impersonation

---

## Waitlist Management

**Route:** `/admin/dashboard/waitlist`

The waitlist holds email addresses from users who joined before launch (when platform mode is `WAITLIST_WITH_INVITES`).

### View Entries
See all waitlist emails with their join date and email-sent status.

### Send Launch Emails
Select users and send them the "We've Launched!" email. This:
- Sends the launch email template with a sign-up link
- Marks each entry as `launchEmailSent`
- Skips users who already received it (no duplicates)

### Send Reminder Emails
For users who received the launch email but have not signed up yet:
- Sends a reminder email
- Only works if the launch email was sent first
- Marks as `reminderEmailSent` to prevent duplicates

---

## Invite Management

**Route:** `/admin/dashboard/invites`

The invite system works with `WAITLIST_WITH_INVITES` mode. Admins create invites to let specific people sign up.

### Create an Invite
1. Enter the user's email address
2. Set expiry days (1 to 90, default is 7)
3. Click "Send Invite"

What happens:
- A unique invite token is generated
- An invitation email is sent with a sign-up link
- The invite appears in the list with its status

### Invite States
- **Active** - Not used, not expired, not revoked
- **Used** - Someone signed up with this invite
- **Expired** - Past the expiry date
- **Revoked** - Manually cancelled by an admin

### Revoke an Invite
If you need to cancel an invite before it is used, click "Revoke". The invite link will stop working immediately.

### Duplicate Protection
- Cannot invite an email that already has an active invite
- Cannot invite an email that already has a user account

---

## How Invites Connect to Sign-Up

1. Admin creates invite for `user@example.com`
2. User receives email with link: `yourdomain.com/auth/invite?token=abc123`
3. User clicks link, token is verified
4. User completes sign-up (Google OAuth or magic link)
5. Invite is marked as used
6. User gets the `USER` role and can access the app

This flow works regardless of which sign-in method the user chooses. The invite just grants them permission to create an account.

---

## Admin Notifications

The admin automatically receives email notifications when:
- A new user signs up (includes user email, name, and signup date)
- A contact form is submitted (includes sender details and message)

These are sent to the `ADMIN_EMAIL` configured in `src/constants/website.ts`:

```typescript
export const WEBSITE = {
    ADMIN_EMAIL: "your@email.com",
    // ...
};
```

# Waitlist & Invites

Source: https://movefa.st/docs/waitlist-and-invites

Collect emails before launch, invite users in batches, and transition from invite-only to public signup.

MoveFast has a built-in system for managing pre-launch access. You can collect emails via a waitlist, invite users selectively, and switch to open signup when you are ready.

## Platform Access Modes

The access mode is set in `src/config/platform.config.ts`:

```typescript
import { PlatformAccessMode } from "@/config/platform.config";

export const PLATFORM_ACCESS_MODE = PlatformAccessMode.WAITLIST_WITH_INVITES;
```

### WAITLIST_WITH_INVITES

This is the default mode. It is designed for pre-launch:

- A waitlist form appears on the landing page
- New users cannot sign up unless they have an invite
- Existing users (already in the database) can still log in
- Admins manage access through the invite system

### OPEN

This is for when you are ready to go public:

- Anyone can sign up freely
- No waitlist form is shown
- No invite required
- Standard SaaS signup experience

---

## The Waitlist Flow

### 1. User Joins the Waitlist

When in `WAITLIST_WITH_INVITES` mode, the landing page shows a waitlist form. When a user submits their email:

- Their email is stored in the `waitingList` collection/table
- They receive a welcome email confirming they are on the list
- Duplicate submissions are blocked

### 2. Admin Reviews Waitlist

In the admin dashboard at `/admin/dashboard/waitlist`, you can see all emails that signed up for the waitlist.

### 3. Admin Sends Launch Emails

When you are ready to let users in, select waitlist entries and send launch emails. The launch email includes:
- A message that the product is live
- A link to the sign-in page

The system tracks who received the launch email to avoid sending duplicates.

### 4. Admin Sends Reminder Emails

For users who got the launch email but have not signed up, you can send a reminder. Reminders only go to users who:
- Already received the launch email
- Have not yet received a reminder
- Are still on the waitlist (have not signed up)

### 5. User Signs Up

When a user signs up through the normal flow:
- They are automatically removed from the waitlist
- Their invite (if one exists) is marked as used

---

## The Invite Flow

Invites give specific people permission to sign up, even before you send a mass launch email.

### Creating an Invite

From the admin dashboard (`/admin/dashboard/invites`):

1. Enter the email address
2. Choose expiry (1 to 90 days, defaults to 7)
3. Click "Send Invite"

The system:
- Generates a unique token
- Stores the invite record
- Sends an email with a sign-up link

### What the User Sees

The invite email contains a link like:

```text
https://yourdomain.com/auth/invite?token=abc123def456
```

When they click it:
1. The token is verified (checks it exists, is not expired, not used, not revoked)
2. They are shown the sign-in page
3. They sign in with Google or magic link
4. The invite is marked as used
5. They now have a full account

### Invite Protection Rules

- Cannot create an invite for someone who already has an account
- Cannot create a duplicate invite for the same email (if one is already active)
- Expired invites cannot be used
- Revoked invites cannot be used
- Admins can revoke any active invite at any time

---

## Transitioning to Open Signup

When you are ready to let everyone in:

1. Open `src/config/platform.config.ts`
2. Change the mode:

```typescript
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.OPEN;
```

3. Deploy the change

That is it. The waitlist form disappears from the landing page, and anyone can sign up without an invite. Existing waitlist data remains in the database (you can clean it up later if you want).

---

## API Endpoints

These endpoints power the waitlist and invite system:

| Endpoint | Method | Access | What it does |
|----------|--------|--------|-------------|
| `/api/waiting-list` | POST | Public | Join the waitlist |
| `/api/waiting-list/send-emails` | POST | Admin | Send launch or reminder emails |
| `/api/admin/waiting-list` | GET | Admin | List all waitlist entries |
| `/api/admin/invites` | GET | Admin | List all invites |
| `/api/admin/invites` | POST | Admin | Create a new invite |
| `/api/admin/invites/[email]` | DELETE | Admin | Delete an invite |
| `/api/admin/invites/[email]/revoke` | POST | Admin | Revoke an invite |
| `/api/auth/verify-invite` | GET | Public | Verify an invite token |
| `/api/auth/signup-with-invite` | POST | Public | Sign up using an invite token |

---

## Tips

- Start with `WAITLIST_WITH_INVITES` mode. It creates urgency and lets you control early access.
- Use waitlist emails to build anticipation. Send launch emails in batches rather than all at once.
- Invites are great for beta testers. Give them access early and collect feedback.
- The waitlist welcome email is a good place to set expectations (timeline, what to expect).
- When you switch to `OPEN`, consider sending one final launch email to remaining waitlist members.

# SEO & Analytics

Source: https://movefa.st/docs/seo-and-analytics

Configure page metadata, Open Graph images, sitemaps, robots.txt, and Google Analytics tracking.

MoveFast includes a complete SEO system, machine-readable public content, and optional Google Analytics integration. Public pages get canonical metadata, social sharing data, structured data, and deliberate crawl directives out of the box.

## Metadata System

MoveFast uses Next.js built-in metadata API with a helper function that generates consistent SEO tags across all pages.

### How It Works

The `createMetadata()` function in `src/lib/seo/metadata.ts` generates:
- Page title (with site name template)
- Meta description
- Keywords
- Canonical URL
- Open Graph tags (title, description, image, type)
- Twitter Card tags
- Robots directives (index/noindex)
- Google preview controls for large images and complete snippets
- RSS, Atom, and JSON Feed discovery links

### Per-Page Metadata

Each page exports metadata using the helper:

```typescript
import { createMetadata } from "@/lib/seo/metadata";

export const metadata = createMetadata({
    title: "Your Page Title",
    description: "A description for search engines and social sharing.",
    path: "/your-page",
    keywords: ["keyword1", "keyword2"],
});
```

### Available Options

```typescript
interface SEOConfig {
    title: string;           // Page title
    description: string;     // Meta description
    path?: string;           // URL path (for canonical URL)
    keywords?: readonly string[]; // SEO keywords
    image?: string;          // OG image URL (defaults to /og-image.svg)
    noIndex?: boolean;       // Set true to hide from search engines
    type?: "website" | "article" | "profile"; // OG type
    publishedTime?: string;  // For blog articles
    modifiedTime?: string;   // For blog articles
    authors?: string[];      // For blog articles
}
```

### Pre-Built Page Metadata

Common pages already have metadata configured via `pageMetadata`:

```typescript
import { pageMetadata } from "@/lib/seo/metadata";

// Use in your page:
export const metadata = pageMetadata.pricing();
export const metadata = pageMetadata.blog();
export const metadata = pageMetadata.contact();
```

---

## Open Graph Image

The default OG image is at `public/og-image.svg`. This is the image shown when your site is shared on Twitter, LinkedIn, Facebook, etc.

To replace it:
1. Create a 1200x630 image
2. Save it as `public/og-image.svg` (or any name)
3. If using a different filename, update `DEFAULT_OG_IMAGE` in `src/lib/seo/metadata.ts`

---

## Sitemap

MoveFast auto-generates a sitemap at `/sitemap.xml`. It includes:
- All public pages (home, pricing, contact, etc.)
- All published blog posts
- All published documentation pages

The sitemap is generated at build time by Next.js from `src/app/sitemap.ts`.

Only canonical, indexable pages belong in the sitemap. Private account routes and legal pages marked `noIndex` are intentionally excluded. Blog entries use their published date as `lastModified`; static pages do not claim a new modification date on every build.

---

## Robots.txt

The `robots.txt` file at `/robots.txt` tells search engines what to crawl. It is generated from `src/app/robots.ts` and:
- Allows all crawlers on public pages
- Blocks crawling of admin, auth, profile, and billing routes
- Allows the public `/api/ai` snapshot and Markdown content endpoints
- Explicitly allows search and user-requested crawlers such as OAI-SearchBot and Claude-SearchBot
- Blocks the listed model-training crawlers by default; change the `TRAINING_BOTS` rule if your policy is different
- Points to the sitemap URL

---

## Structured Data

MoveFast renders JSON-LD that matches visible page content:

- Organization and WebSite entities in the root layout
- WebPage, SoftwareApplication, and FAQPage entities on the home page
- CollectionPage and ItemList data on the blog index
- BlogPosting and BreadcrumbList data on each article
- TechArticle and BreadcrumbList data on documentation pages

The builders live in `src/lib/seo/structured-data.ts`. The `JsonLd` component escapes `<` characters before rendering JSON so content cannot break out of the script tag.

Keep structured data aligned with what a visitor can actually see. Do not add reviews, ratings, prices, or features that are not present on the corresponding page.

---

## AI and Agent Discovery

The HTML pages remain the canonical source. The additional discovery endpoints make the same public information easier for agents and feed readers to retrieve:

| Endpoint | Purpose |
| --- | --- |
| `/llms.txt` | Concise site identity and curated first-party content map |
| `/llms-full.txt` | Combined public documentation and blog context |
| `/ai.txt` | Retrieval, attribution, freshness, and restricted-route guidance |
| `/api/ai` | Bounded JSON snapshot of public pages and recent content |
| `/api/md/docs/[slug]` | Clean Markdown for a published documentation page |
| `/api/md/blogs/[slug]` | Clean Markdown for a published blog article |
| `/.well-known/api-catalog` | Machine-readable catalog of public discovery endpoints and feeds |

These files supplement normal SEO. They do not replace crawlable HTML, internal links, accurate metadata, or useful first-party content.

The global HTTP `Link` header in `next.config.ts` advertises the discovery files and feeds without adding route-specific client code.

---

## Blog Feeds

Published blogs are available as:

- RSS at `/blogs/rss.xml`
- Atom at `/blogs/atom.xml`
- JSON Feed at `/blogs/feed.json`

All three feeds are generated from the same MDX frontmatter used by the blog pages, so titles, dates, excerpts, authors, and canonical URLs stay synchronized.

---

## Google Analytics

MoveFast supports Google Analytics 4 (GA4) through a dedicated component.

### Setup

1. Create a GA4 property at [analytics.google.com](https://analytics.google.com)
2. Get your Measurement ID (starts with `G-`)
3. Add it to your environment:

```env
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
```

That is it. The analytics script loads automatically on all pages.

### How It Works

The `GoogleAnalytics` component in `src/components/analytics/GoogleAnalytics.tsx` injects the GA4 script tag in the root layout. It only loads when `NEXT_PUBLIC_GA_MEASUREMENT_ID` is set and non-empty.

### Disabling Analytics

To disable analytics (e.g., in development), simply do not set the `NEXT_PUBLIC_GA_MEASUREMENT_ID` variable, or leave it empty.

---

## Page Titles

The root layout sets a title template:

```typescript
title: {
    default: "MoveFast",
    template: "%s | MoveFast",
}
```

This means:
- The home page title is just "MoveFast"
- Other pages are formatted as "Page Title | MoveFast"

To change the site name, update `WEBSITE.NAME` in `src/constants/website.ts`.

---

## Legal Pages and noIndex

Legal pages (privacy policy, terms of service, cookie policy) are marked as `noIndex: true` so search engines do not index them. This keeps your search results clean and focused on product pages.

```typescript
export const metadata = createMetadata({
    title: "Privacy Policy",
    description: "...",
    noIndex: true,  // Won't appear in search results
});
```

---

## Blog SEO

Blog posts get full article-type metadata and matching BlogPosting JSON-LD including:
- Published date
- Modified date
- Author name
- Article-specific OG type
- Featured image as OG image

This is handled automatically from the blog post frontmatter (title, description, date, author, image fields).

---

## Tips

- Always set unique `title` and `description` for every page. Duplicate metadata hurts rankings.
- Keep descriptions concise, specific, and accurate; search engines may display a different snippet when it better answers the query.
- Use descriptive, keyword-rich titles but keep them natural.
- Replace the default OG image with something branded. It is the first thing people see when your link is shared.
- Submit your sitemap to Google Search Console after deploying for faster indexing.
- Keep `NEXT_PUBLIC_WEBSITE_URL` set to the production origin so canonical URLs, feeds, and structured data never point to localhost.

# Customization

Source: https://movefa.st/docs/customization

Change branding, colors, pricing, legal pages, email templates, and add new pages to make MoveFast your own.

MoveFast is designed to be customized quickly. Most changes involve editing a few config files rather than digging through the codebase.

## Branding

### Site Name and URLs

Edit `src/constants/website.ts`:

```typescript
export const WEBSITE = {
    NAME: "YourApp",                          // Shown in UI, emails, metadata
    URL: PUBLIC_ENV.WEBSITE_URL,              // Set via NEXT_PUBLIC_WEBSITE_URL env var
    SUPPORT_EMAIL: "support@yourapp.com",     // Shown in footer, legal pages
    ADMIN_EMAIL: "admin@yourapp.com",         // Receives notification emails
    ADDRESS: "Your Company Address",          // Used in legal pages
    TWITTER_URL: "https://twitter.com/yourapp",
    GITHUB_URL: "https://github.com/yourapp",
    LINKEDIN_URL: "https://linkedin.com/company/yourapp",
};
```

This single file controls the brand name everywhere: the navbar, footer, emails, metadata, and legal pages.

### Logo

Replace these files in `public/`:
- `logo.png` - Main logo (used in navbar, light mode)
- `logo-white.png` - White version (used on dark backgrounds)
- `og-image.svg` - Social sharing image (1200x630)
- `web-app-manifest-192x192.png` - PWA icon small
- `web-app-manifest-512x512.png` - PWA icon large

### Colors and Theme

MoveFast uses Tailwind CSS 4 with CSS variables for theming. The color system supports both light and dark modes automatically via `next-themes`.

To change colors, edit the CSS variables in your global stylesheet (`src/app/globals.css`). The key variables are:

```css
:root {
    --primary: ...;
    --primary-foreground: ...;
    --background: ...;
    --foreground: ...;
    /* etc. */
}
```

If you are using shadcn/ui's theming system, you can generate new color themes at [ui.shadcn.com/themes](https://ui.shadcn.com/themes).

---

## Pricing Configuration

Everything about pricing lives in `src/config/pricing.config.ts`:

### Change Payment Model

```typescript
// One-time payments (lifetime access)
export const PAYMENT_MODEL = PaymentModel.ONE_TIME;

// Recurring subscriptions
export const PAYMENT_MODEL = PaymentModel.SUBSCRIPTION;
```

### Change Currency

```typescript
export const DEFAULT_CURRENCY = "EUR";   // ISO 4217 code (sent to payment provider)
export const CURRENCY_SYMBOL = "€";      // Displayed in the UI
```

### Add or Remove Pricing Tiers

Edit the `PRICING_PLANS` array. Each plan has:

```typescript
{
    name: "Plan Name",
    description: "Short description",
    productIds: {
        monthly: "provider-product-id",
        yearly: "provider-product-id",
        oneTime: "provider-product-id",
    },
    price: {
        monthly: 29,    // Monthly price
        yearly: 23,     // Per-month price when billed yearly
    },
    oneTimePrice: 149,   // One-time payment price
    highlighted: true,   // Visual emphasis
    badge: "Most Popular",
    features: [
        { name: "Feature name", description: "Optional detail", included: true },
        { name: "Not included feature", included: false },
    ],
    cta: {
        text: "Get Started",
        variant: "default",  // "default" or "outline"
    },
}
```

### Free Tier

Use `"free"` as the product ID for a free plan:

```typescript
productIds: {
    monthly: "free",
    yearly: "free",
}
```

Free plans skip the checkout flow entirely.

---

## Platform Access Mode

Control who can sign up in `src/config/platform.config.ts`:

```typescript
// Before launch: invite-only with waitlist
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.WAITLIST_WITH_INVITES;

// After launch: open signup
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.OPEN;
```

---

## Landing Page Sections

The landing page is composed of sections in `src/components/landing/`:

- `HeroSection` - Main headline, CTA button, trust elements
- `FeaturesSection` - Feature highlights
- `UseCasesSection` - Use case cards
- `PricingSection` - Pricing plans with billing toggle
- `FAQSection` - Accordion-based FAQ
- `CTASection` - Final call to action

Each section is a standalone component. You can reorder them, remove ones you do not need, or add new ones by editing `src/app/(main)/page.tsx`.

---

## Blog Posts

Add new blog posts as MDX files in `content/blog/`:

```mdx
---
title: "Your Blog Post Title"
description: "A short description for SEO"
date: "2025-01-15"
tags: ["saas", "startup"]
published: true
pinned: false
author: "Your Name"
authorImage: "/images/authors/you.jpg"
image: "/images/blog/your-post-image.png"
---

Your blog content here. Supports **markdown** and React components.
```

Blog posts are automatically:
- Listed at `/blogs` with tag filtering
- Accessible at `/blogs/[slug]`
- Included in the sitemap
- Given proper SEO metadata

---

## Legal Pages

Legal page content is at:
- `src/app/(main)/privacy-policy/page.tsx`
- `src/app/(main)/terms-of-service/page.tsx`
- `src/app/(main)/cookie-policy/page.tsx`

These pull the company name, address, and support email from `src/constants/website.ts`. Update the actual legal text to match your product and jurisdiction.

---

## Adding New Pages

To add a new public page:

1. Create a folder in `src/app/(main)/your-page/`
2. Add a `page.tsx`:

```typescript
import { createMetadata } from "@/lib/seo/metadata";

export const metadata = createMetadata({
    title: "Your Page",
    description: "Description for SEO",
    path: "/your-page",
});

export default function YourPage() {
    return (
        <div className="container mx-auto px-4 py-20">
            <h1>Your Page</h1>
        </div>
    );
}
```

3. Optionally add it to navigation in the navbar/footer components

For protected pages, add auth checking:

```typescript
import { checkAuth } from "@/lib/auth/page-auth";
import { AccessLevel } from "@/lib/types/auth.enums";

export default async function ProtectedPage() {
    const user = await checkAuth({ access: AccessLevel.AUTHENTICATED });
    // user is guaranteed to exist here
    return <div>Hello {user.name}</div>;
}
```

---

## Email Templates

Customize the look and content of emails in the `emails/` folder. The shared components (`emails/components/EmailHeader.tsx` and `EmailFooter.tsx`) control the header logo and footer links across all emails.

Preview changes with:

```bash
npm run email:dev
```

---

## Dark Mode

Dark mode is handled by `next-themes` and works automatically. The theme toggle component lets users switch between light, dark, and system themes.

To set a default theme, edit the `ThemeProvider` in `src/components/theme-provider.tsx`.

# Deployment

Source: https://movefa.st/docs/deployment

Deploy MoveFast to Vercel with proper environment variables, webhook URLs, and production security settings.

MoveFast is optimized for Vercel deployment. This guide covers everything you need to go from local development to production.

## Deploy to Vercel

### 1. Push to GitHub

Make sure your code is in a Git repository:

```bash
git init
git add .
git commit -m "Initial commit"
git remote add origin <your-repo-url>
git push -u origin main
```

### 2. Import to Vercel

1. Go to [vercel.com/new](https://vercel.com/new)
2. Import your Git repository
3. Vercel will detect it as a Next.js project automatically
4. Click "Deploy"

### 3. Configure Environment Variables

In your Vercel project settings (Settings > Environment Variables), add all variables from your `.env` file. At minimum:

```env
# App
NEXT_PUBLIC_WEBSITE_URL=https://yourdomain.com
NEXTAUTH_URL=https://yourdomain.com
AUTH_SECRET=your-production-secret
AUTH_TRUST_HOST=true

# Database (pick one based on your variant)
MONGODB_URI=mongodb+srv://...
# or
DATABASE_URL=postgresql://...

# Email (pick one based on your variant)
RESEND_API_KEY=re_...
# or
POSTMARK_API_KEY=...
# or
SMTP_HOST=...
SMTP_USERNAME=...
SMTP_PASSWORD=...

EMAIL_FROM_EMAIL=noreply@yourdomain.com
EMAIL_FROM_NAME=YourApp

# Payment (pick one based on your variant)
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# or
DODO_API_KEY=...
DODO_WEBHOOK_SECRET=...
DODO_ENVIRONMENT=live_mode
# or
POLAR_ACCESS_TOKEN=polar_at_...
POLAR_WEBHOOK_SECRET=...
POLAR_ENVIRONMENT=production

# OAuth
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
```

### 4. Custom Domain

1. In Vercel, go to Settings > Domains
2. Add your domain
3. Update DNS records as instructed by Vercel
4. Update `NEXT_PUBLIC_WEBSITE_URL` and `NEXTAUTH_URL` to use the new domain

---

## Supabase Variants: Extra Build Step

If you are using a Supabase/PostgreSQL variant, Prisma needs to generate the client before building. Add this to your Vercel build command:

```text
npx prisma generate && npm run build
```

Or set it in `package.json`:

```json
{
  "scripts": {
    "vercel-build": "npx prisma generate && next build"
  }
}
```

---

## Webhook URLs

After deployment, set up your webhook endpoint in your payment provider dashboard:

**Stripe:**
```text
https://yourdomain.com/api/payment/webhook/stripe
```

**Dodo Payments:**
```text
https://yourdomain.com/api/payment/webhook/dodo
```

**Polar:**
```text
https://yourdomain.com/api/payment/webhook/polar
```

Make sure the webhook secret in your env matches the one generated by your provider.

---

## Google OAuth Redirect

Add your production domain to Google OAuth redirect URIs:

```text
https://yourdomain.com/api/auth/callback/google
```

Do this in Google Cloud Console > APIs & Services > Credentials > Your OAuth Client.

---

## Production Checklist

Run through this before going live:

### URLs and Auth
- [ ] `NEXT_PUBLIC_WEBSITE_URL` points to your production domain
- [ ] `NEXTAUTH_URL` points to your production domain
- [ ] `AUTH_SECRET` is a unique, randomly generated value (not reused from dev)
- [ ] Google OAuth redirect includes production domain

### Database
- [ ] Database connection string points to production database
- [ ] MongoDB: IP whitelist includes `0.0.0.0/0` for Vercel (serverless IPs)
- [ ] Supabase: Using pooled connection string for serverless

### Payments
- [ ] Using live/production API keys (not test keys)
- [ ] Webhook endpoint is registered in provider dashboard
- [ ] Webhook secret matches what is in Vercel env vars
- [ ] Product IDs in `pricing.config.ts` match real products in provider dashboard
- [ ] Tested a real checkout flow end to end

### Email
- [ ] Using a verified sending domain (not a free email address)
- [ ] Email templates have correct branding
- [ ] Magic link emails are being delivered (check spam folders)

### Branding
- [ ] `WEBSITE.NAME` in `src/constants/website.ts` is your app name
- [ ] `WEBSITE.SUPPORT_EMAIL` is a real support address
- [ ] OG image (`public/og-image.svg`) has your branding
- [ ] Legal pages have your actual terms, privacy policy, etc.

### Platform Mode
- [ ] Access mode is set correctly (`OPEN` for public, `WAITLIST_WITH_INVITES` for pre-launch)
- [ ] Admin user is created in the production database

---

## Environment Variable Separation

Vercel lets you set different env variables for different environments:
- **Production** - Your live site
- **Preview** - Deploy previews from pull requests
- **Development** - Used with `vercel dev`

Recommendation:
- Use production payment keys only in Production
- Use test/sandbox keys in Preview and Development
- Use a separate database for Preview to avoid polluting production data

---

## Monitoring and Logs

### Vercel Logs
View runtime logs in Vercel Dashboard > Your Project > Logs. This shows:
- API route execution
- Server-side rendering
- Errors and warnings

### Built-in Logger
MoveFast has a logger (`src/lib/logger.ts`) that outputs structured logs. Enable it in production:

```env
ENABLE_LOGGING=true
```

Log levels: `debug`, `info`, `warn`, `error`. Set minimum level with:

```env
LOG_LEVEL=info
```

---

## Other Hosting Providers

While optimized for Vercel, MoveFast works on any platform that supports Next.js:

- **Netlify** - Similar to Vercel, supports Next.js
- **Railway** - Good for full-stack apps with databases
- **Render** - Supports Node.js with static builds
- **Docker** - Self-host with `next build` and `next start`
- **AWS Amplify** - AWS-managed hosting

For self-hosting, run:

```bash
npm run build
npm run start
```

The app starts on port 3000 by default. Use the `PORT` environment variable to change it.

---

## Redeployment

Every time you push to your main branch, Vercel automatically rebuilds and deploys. For manual redeployments:

1. Go to Vercel Dashboard > Your Project > Deployments
2. Click "Redeploy" on the latest deployment

If you change environment variables, you need to redeploy for them to take effect.

# API Reference

Source: https://movefa.st/docs/api-reference

Complete reference for all API routes including authentication, users, payments, subscriptions, waitlist, and admin endpoints.

All API routes live under `/api/`. They return a consistent JSON response format:

**Success:**
```json
{
  "success": true,
  "payload": { ... },
  "message": "Optional message"
}
```

**Error:**
```json
{
  "success": false,
  "error": "Error description",
  "code": "OPTIONAL_ERROR_CODE"
}
```

---

## Authentication

### Send Magic Link

```text
POST /api/auth/magic-link/send
```

Sends a magic link email to the user. Works for both new and existing users.

**Body:**
```json
{
  "email": "user@example.com",
  "callbackUrl": "/dashboard"  // optional, where to redirect after login
}
```

**Access:** Public

---

### Verify Invite Token

```text
GET /api/auth/verify-invite?token=abc123
```

Checks if an invite token is valid (not expired, not used, not revoked).

**Response:**
```json
{
  "success": true,
  "payload": {
    "email": "invited@example.com",
    "expiresAt": "2025-01-20T00:00:00Z"
  }
}
```

**Access:** Public

---

### Sign Up With Invite

```text
POST /api/auth/signup-with-invite
```

Creates a user account using a valid invite token.

**Body:**
```json
{
  "token": "invite-token-here"
}
```

**Access:** Public

---

## User Profile

### Get Profile

```text
GET /api/profile
```

Returns the currently authenticated user's profile.

**Response:**
```json
{
  "success": true,
  "payload": {
    "id": "user-id",
    "name": "John Doe",
    "email": "john@example.com",
    "image": "https://...",
    "roles": ["USER"],
    "isActive": true,
    "planId": "product-id-or-null",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}
```

**Access:** Authenticated

---

### Update Profile

```text
PUT /api/profile
```

Updates the current user's profile information.

**Body:**
```json
{
  "name": "New Name"
}
```

**Access:** Authenticated

---

## Payments

### Create Checkout Session

```text
POST /api/payment/checkout
```

Creates a checkout session with the configured payment provider. Redirects the user to the provider's hosted checkout page.

**Body:**
```json
{
  "productId": "your-product-id",
  "billingPeriod": "monthly"  // "monthly" or "yearly" (subscription model only)
}
```

**Response:**
```json
{
  "success": true,
  "payload": {
    "sessionId": "session-id",
    "checkoutUrl": "https://checkout.provider.com/...",
    "provider": "STRIPE"
  }
}
```

**Access:** Authenticated

**Notes:**
- For one-time payment model, `billingPeriod` is ignored
- Returns 409 if user already has an active subscription (subscription model)

---

### Get Payment History

```text
GET /api/payment/history
```

Returns all payments for the current user.

**Response:**
```json
{
  "success": true,
  "payload": [
    {
      "id": "payment-id",
      "provider": "STRIPE",
      "status": "COMPLETED",
      "amount": 149,
      "currency": "USD",
      "planId": "product-id",
      "createdAt": "2025-01-15T00:00:00Z"
    }
  ]
}
```

**Access:** Authenticated

---

### Payment Webhooks

```text
POST /api/payment/webhook/stripe
POST /api/payment/webhook/dodo
POST /api/payment/webhook/polar
```

Receives webhook events from the payment provider. Only one of these exists in your variant.

**Access:** Public (verified by webhook signature)

**Notes:**
- Do not call these manually. They are called by your payment provider.
- The webhook secret in your env vars must match the one configured in the provider dashboard.

---

## Subscriptions

### Get Active Subscription

```text
GET /api/subscription
```

Returns the current user's active subscription, or null if they do not have one.

**Response:**
```json
{
  "success": true,
  "payload": {
    "id": "sub-id",
    "provider": "STRIPE",
    "status": "ACTIVE",
    "planId": "product-id",
    "currentPeriodStart": "2025-01-01T00:00:00Z",
    "currentPeriodEnd": "2025-02-01T00:00:00Z",
    "cancelAt": null
  }
}
```

**Access:** Authenticated

---

### Get Billing Portal URL

```text
GET /api/subscription/billing-portal?returnUrl=/payments
```

Returns a URL to the payment provider's customer portal where users can manage their subscription.

**Query params:**
- `returnUrl` - Where to redirect after the user is done (required)

**Response:**
```json
{
  "success": true,
  "payload": {
    "url": "https://billing.provider.com/portal/..."
  }
}
```

**Access:** Authenticated (must have an active subscription)

---

## Waiting List

### Join Waitlist

```text
POST /api/waiting-list
```

Adds an email to the waitlist. Only works when platform mode is `WAITLIST_WITH_INVITES`.

**Body:**
```json
{
  "email": "user@example.com"
}
```

**Access:** Public

**Notes:**
- Returns 409 if email is already on the waitlist
- Sends a welcome email automatically

---

### Send Emails to Waitlist (Admin)

```text
POST /api/waiting-list/send-emails
```

Sends launch or reminder emails to selected waitlist entries.

**Body:**
```json
{
  "emails": ["user1@example.com", "user2@example.com"],
  "type": "launch"  // "launch" or "reminder"
}
```

**Response:**
```json
{
  "success": true,
  "payload": {
    "sent": 5,
    "failed": 1
  }
}
```

**Access:** Admin only

---

## Contact

### Submit Contact Form

```text
POST /api/contact
```

Sends a contact form notification email to the admin.

**Body:**
```json
{
  "name": "John Doe",
  "email": "john@example.com",
  "message": "I have a question about..."
}
```

**Access:** Public

---

## Admin Endpoints

### List All Users

```text
GET /api/users
```

Returns all registered users.

**Access:** Admin only

---

### Toggle User Status

```text
POST /api/users/:userId/toggle-status
```

Enables or disables a user account. Cannot disable admin users.

**Access:** Admin only

---

### Start Impersonation

```text
POST /api/admin/impersonate
```

Start impersonating a user. Sets a signed cookie.

**Body:**
```json
{
  "targetUserId": "user-id-to-impersonate"
}
```

**Access:** Admin only

---

### Get Impersonation Status

```text
GET /api/admin/impersonate
```

Check if currently impersonating someone.

**Access:** Authenticated

---

### Stop Impersonation

```text
DELETE /api/admin/impersonate
```

Stop impersonating and return to admin view.

**Access:** Authenticated

---

### List Waitlist Entries (Admin)

```text
GET /api/admin/waiting-list
```

Returns all waitlist entries with their status.

**Access:** Admin only

---

### List Invites

```text
GET /api/admin/invites
```

Returns all invite records.

**Access:** Admin only

---

### Create Invite

```text
POST /api/admin/invites
```

Creates a new invite and sends the invitation email.

**Body:**
```json
{
  "email": "newuser@example.com",
  "expiresInDays": 7
}
```

**Access:** Admin only

**Notes:**
- Returns 409 if user already exists or invite already active

---

### Delete Invite

```text
DELETE /api/admin/invites/:email
```

Permanently deletes an invite record.

**Access:** Admin only

---

### Revoke Invite

```text
POST /api/admin/invites/:email/revoke
```

Marks an invite as revoked (keeps the record but makes it unusable).

**Access:** Admin only

---

## Error Codes

Common error responses you may encounter:

| Status | Meaning |
|--------|---------|
| 400 | Bad request (validation failed or invalid input) |
| 401 | Not authenticated (no valid session) |
| 403 | Forbidden (insufficient role or account disabled) |
| 404 | Resource not found |
| 409 | Conflict (duplicate resource) |
| 500 | Internal server error |

Some errors include a `code` field for programmatic handling:

```json
{
  "error": "You already have an active subscription",
  "code": "ACTIVE_SUBSCRIPTION_EXISTS"
}
```

---

## Making API Calls from the Frontend

MoveFast uses an axios client pre-configured with the base URL. Import it and use the `API_ROUTES` constants:

```typescript
import { axiosClient } from "@/lib/axios";
import { API_ROUTES } from "@/constants/api-routes";

// Example: fetch payment history
const response = await axiosClient.get(API_ROUTES.PAYMENT.HISTORY);
const payments = response.data.payload;

// Example: create checkout
const response = await axiosClient.post(API_ROUTES.PAYMENT.CHECKOUT, {
    productId: "your-product-id",
});
const { checkoutUrl } = response.data.payload;
window.location.href = checkoutUrl;
```

The axios client automatically includes cookies for authentication and handles the `/api` prefix.



# 10x Your Ship Speed: The Modern SaaS Architecture

Source: https://movefa.st/blogs/10x-your-ship-speed
Published: 2026-05-20
Author: Kenil

Stop reinventing the wheel. Discover the battle-tested Next.js stack that top founders are using to launch production-ready apps in days, not months.

Every SaaS founder knows the feeling. You have a killer idea, your head's buzzing with product vision, you open your editor and then you spend the next six weeks wiring up authentication, wrestling with payment webhooks, and debugging email delivery.

By the time you write a single line of actual business logic, you've burned through **~40 hours of dev time** and potentially **$200+ in Claude Code or LLM tokens** just generating boilerplate. Your motivation is shot, your launch window is closing, and you haven't even validated the idea yet.

That's the trap. And that's exactly why [MoveFast](https://movefa.st) exists.

## The Real Cost of "I'll Just Build It Myself"

Let's be honest about what "starting from scratch" actually looks like in 2026. Even with AI-assisted coding, a production-grade SaaS foundation takes real time:

| Task | Estimated Time |
|------|---------------|
| Authentication (OAuth, magic links, sessions) | ~10 hours |
| Payment integration (webhooks, billing logic) | ~12 hours |
| Email infrastructure (templates, providers) | ~8 hours |
| Database + Admin dashboard | ~10 hours |
| **Total before writing business logic** | **~40 hours** |

And here's the kicker: after all that work, is it bug-free? **Not guaranteed.** You're building plumbing, not product. Every hour spent on auth middleware is an hour not spent on the feature that makes your SaaS worth paying for.

MoveFast is a production-ready Next.js SaaS boilerplate that lets you skip straight to the work that actually matters for a **one-time payment of $90**. No subscriptions, no per-seat pricing, no recurring drain on your runway.

## The Stack: What You Actually Get

MoveFast isn't a toy template with a landing page and a TODO list. It's built on **Next.js 16** with **React 19**, **TypeScript**, and **Tailwind CSS 4**. Here's what's inside.

### 1. Next.js 16 + shadcn/ui

The foundation is Next.js 16 with the App Router. You get:

- **Full TypeScript** across the entire codebase. No `any` types, no guessing.
- **SEO optimization** baked in with proper metadata, Open Graph, sitemaps, and robots.txt.
- **Responsive design** with Tailwind CSS 4 utility classes.
- **Dark and light themes** via next-themes, ready out of the box.
- **shadcn/ui components** built on Radix UI. Accessible, beautiful, and fully yours. No version lock-in, no breaking updates when a dependency bumps.

The App Router's React Server Components mean your data fetching happens on the server, right where your UI renders. Your users see fully loaded pages instantly. No client-side waterfalls, no loading spinner hell.

### 2. Authentication System

Auth is the first thing every SaaS needs, and it's the most dangerous thing to get wrong. MoveFast ships with **Auth.js (NextAuth v5)** pre-configured with:

- **Magic Links** via email. Users click a link, they're in. No password resets, no credential stuffing attacks.
- **Google OAuth** for one-click sign-in.
- **Role-based access control** with USER and ADMIN roles built in.
- **Platform access modes** letting you control whether signup is open or invite-only.
- **Admin impersonation** so you can see the app exactly as any user sees it for debugging.

The entire auth flow (sign up, sign in, session management, profile updates, account disabling) is done.

### 3. Payment Processing: Pick Your Provider

MoveFast supports three payment providers out of the box through a clean, provider-agnostic interface:

- **Stripe** for the most popular payment processor
- **Polar** for developer-focused billing
- **Dodo Payments** as a merchant of record (handles taxes for you)

All three support both **one-time payments and subscriptions** with proper webhook handling, signature verification, and a billing portal for self-service subscription management.

```typescript
// MoveFast's payment provider interface - one abstraction, three providers
interface IPaymentProvider {
    createOneTimePayment(params): Promise<CheckoutSession>;
    createSubscription(params): Promise<CheckoutSession>;
    cancelSubscription(subscriptionId): Promise<void>;
    getSubscription(subscriptionId): Promise<SubscriptionDetails>;
    handleWebhookEvent(payload, signature): Promise<WebhookHandleResult>;
    createBillingPortalSession(customerId): Promise<{ url: string }>;
}
```

You pick your variant when you clone. No dead code. No unused payment providers scattered across your codebase.

### 4. Admin Dashboard & Analytics

Most boilerplates skip this entirely. MoveFast doesn't. You get a full **admin dashboard** with:

- **User management** with enable/disable controls and role assignment.
- **Waitlist management** with bulk email sending (launch + reminder emails).
- **Invite system** to control who can sign up during pre-launch.
- **Growth analytics** with user growth charts and real-time metrics.
- **User impersonation** to log in as any user and debug issues in their exact context.

This is the kind of tooling that separates "I launched a landing page" from "I'm running a real product."

### 5. Email System: Pick Your Provider

Transactional email is deceptively complex. MoveFast gives you three options:

- **Resend** for a modern, developer-first email API.
- **Postmark** for best-in-class deliverability.
- **Nodemailer** for any SMTP server (Gmail, AWS SES, etc.).

It comes with **7 React Email templates** pre-built: magic link delivery, welcome emails, admin notifications, user invitations, waitlist communications, and contact form alerts. Beautiful, responsive HTML emails that you can preview locally with `npm run email:dev`.

### 6. Database: Pick Your Storage

Two database options, both fully wired with repositories and adapters:

- **MongoDB + Mongoose** for document-based, schema-flexible storage.
- **Supabase/PostgreSQL + Prisma 7** for relational data with type-safe queries.

Both use a clean **repository pattern** with factory functions, so swapping databases doesn't touch your business logic.

### 7. Waitlist & Launch Mode

MoveFast includes a built-in **waitlist and invite system**. Collect emails before launch, send invites in batches, then flip a config to open public signup when you're ready. The entire launch flow is handled:

- Waitlist form on the landing page
- Welcome email on signup
- Admin-controlled launch and reminder emails
- Token-based invite system with expiry and revocation
- One-line switch from invite-only to open signup

## 18 Variants: Your Exact Stack

This is probably the most underrated feature. MoveFast generates **18 clean variants** from every combination of:

- **Database:** MongoDB or Supabase
- **Email:** Resend, Postmark, or Nodemailer
- **Payment:** Stripe, Polar, or Dodo Payments

When you clone, you pick the branch that matches your stack:

```bash
git clone -b mongodb-resend-stripe git@github.com:movefa-st/code.git my-saas
```

No dead code. No unused providers. No factory conditionals at runtime. Each variant is a clean, focused codebase with only the integrations you chose compiled in.

## The Math That Matters

Let's do the napkin math:

- **Building it yourself:** ~40 hours of work + ~$200 in LLM tokens + unknown number of bugs = weeks of delay
- **Using MoveFast:** $90 one-time payment + 30 minutes of setup = shipping your actual product today

That $90 buys you **lifetime access** to every future update and improvement. Choose your stack from 18 variants, clone, configure your env, and start building the thing that makes your product unique.

## Stop Building Plumbing

The best SaaS founders in 2026 aren't the ones who can configure payment webhooks the fastest. They're the ones who **skip the infrastructure entirely** and pour every hour into the thing that makes their product unique.

MoveFast is the Next.js base you shouldn't build yourself. Auth, payments, email, database, admin dashboard, waitlist, it's all there. Production-ready. Type-safe. Tested across 18 variant combinations.

**Your next SaaS is one `git clone` away.**

# From Idea to First Dollar in 48 Hours

Source: https://movefa.st/blogs/idea-to-first-dollar
Published: 2026-05-15
Author: Kenil

The playbook for testing market validation quickly. Learn how a streamlined MVP process can prevent you from building features nobody wants.

You have a SaaS idea. It's been rattling around your head for weeks. You can already picture the dashboard, the onboarding flow, the pricing page.

So you open your editor and start building. Auth system: two days. Payment checkout integration: another three. Email transactional templates, database schema, admin panel... suddenly it's been six weeks, you've burned through $200 in LLM API tokens trying to vibe-code the plumbing, and you still haven't shown a single human being what you're building.

Then you launch. **Crickets.**

This is the most expensive mistake in SaaS: building something nobody wants. The fix isn't building *better*. It's building *faster*, so failure costs you a weekend instead of a quarter.

Here's the exact playbook to go from idea to first paying customer in 48 hours.

## Why 48 Hours Is the Right Constraint

A tight deadline isn't about hustle culture. It's a forcing function. When you only have 48 hours, you can't afford scope creep, pixel-perfect obsession, or "nice-to-have" features. You're forced to answer the only question that matters early on:

**Will someone pay real money for this?**

Your first version will be wrong. It doesn't matter if you spend 48 hours or 48 weeks. The market will surprise you. The only variable you control is how quickly you find out.

## The 48-Hour Playbook

### Hours 1-4: Scope Like a Scientist

Most founders skip this step. Don't. Sit down and answer three questions with brutal specificity:

1. **Who is your customer?** Not "small businesses." Try: *"Solo founders building B2B tools who waste 10+ hours a week on invoicing and payment reconciliation."*

2. **What's their #1 pain?** One pain. The kind that makes someone say *"I'd pay money right now to make this go away."*

3. **What is the absolute minimum you can build to solve it?** If you're building an AI writing tool, you don't need a rich-text editor with collaboration. You need a text box, an API call, and a decent output. Everything else is a distraction.

Now write down a hypothesis:

> *"Freelance developers will pay $29/month for a tool that auto-generates and sends invoices from their GitHub commit history."*

You're not planning a company. You're designing an experiment.

### Hours 5-8: Set Up the Foundation (Not From Scratch)

Here's where most 48-hour sprints die. You sit down to build and realize you need auth, payments, a database, transactional email, and an admin view just to have something launchable. That's roughly 40 hours of boilerplate work:

| Infrastructure Layer | Typical Build Time |
|---|---|
| Auth (OAuth + Magic Links) | ~10 hours |
| Payments (Checkout + Webhooks) | ~12 hours |
| Transactional Email | ~8 hours |
| Database + Admin Dashboard | ~10 hours |
| **Total** | **~40 hours** |

Your 48-hour window is already blown, and you haven't written a single line of product logic.

This is exactly why [MoveFast](https://movefa.st) exists. It's a production-ready Next.js 16 boilerplate with 18 stack variants covering MongoDB/Supabase, Resend/Postmark/Nodemailer, and Stripe/Polar/Dodo Payments. Pick the combination that fits your stack, clone the branch, and start from working flows:

```bash
# Pick your stack variant (e.g. MongoDB + Resend + Stripe)
git clone -b mongodb-resend-stripe git@github.com:movefa-st/code.git my-mvp
cd my-mvp
npm install

# Configure environment
cp .env.example .env
# Add your provider keys and database URI

npm run dev
```

Within minutes (not days) you have:

- **Authentication** via Auth.js with Magic Links and Google OAuth
- **Payments** through Stripe, Polar, or Dodo Payments (depending on your variant)
- **Transactional email** with React Email templates via Resend, Postmark, or Nodemailer
- **Database** with MongoDB + Mongoose or Supabase + Prisma
- **Admin Dashboard** with analytics, user management, and impersonation
- **Waitlist & Launch Mode** with invite system built in
- **UI components** from shadcn/ui with dark/light themes

That's 40 hours of plumbing reduced to 40 minutes of configuration. The rest of your build window is now free for the thing that actually matters: your product's core logic.

### Hours 9-24: Build the Core Feature

You now have ~16 uninterrupted hours to build the one thing your product does. Not three things. Not five. **One.**

Rules for this phase:

- **No refactoring.** The code doesn't need to be pretty. It needs to work.
- **No design rabbit holes.** shadcn/ui already looks good. Use it as-is.
- **No tests yet.** Validation comes from customers, not test suites.
- **No additional integrations.** You already have auth, payments, and email. Stay focused.

If the pain you're solving is real, your users will tolerate a slightly rough UI. They won't tolerate a product that doesn't exist because you spent three days perfecting button hover states.

### Hours 25-36: Ship a Landing Page That Converts

Your landing page has one job: communicate value so clearly that a stranger pulls out their credit card.

Here's the anatomy of a page that works:

| Section | What It Does |
|---|---|
| **Hero headline** | State the value prop in one sentence, no jargon |
| **Demo GIF or video** | Show the product working, 30 seconds max |
| **3 benefit bullets** | Time saved, money earned, pain removed |
| **Social proof** | Even one testimonial or "Used by X teams" |
| **Pricing** | Be transparent, show the number upfront |
| **CTA** | One clear action: "Start Free Trial" or "Buy Now" |

Skip the blog, the team page, the mission statement. Those are for later. Right now the page answers one question: *"Is this worth my money?"*

If you're not ready for paying users on Day 1, MoveFast's **built-in Waitlist & Launch Mode** lets you collect emails and build an audience while you finish up. It's a config toggle: set `PLATFORM_ACCESS_MODE` to `WAITLIST_WITH_INVITES`, and your app collects signups. When you're ready, switch to `OPEN` and invite people in batches from the admin dashboard.

### Hours 37-48: Launch and Sell

Forget the big Product Hunt launch. Go direct.

**Your outreach playbook:**

- **50 DMs on X or LinkedIn.** Find people who fit your target persona. Personalize every message. *"Hey Sarah, I saw you tweet about time tracking. I just built a tool that generates invoices from GitHub commits. Want to try it free for a week?"*
- **3-5 niche communities.** Reddit, Discord servers, Slack groups, Indie Hackers. Wherever your users already hang out.
- **20 cold emails.** Short, specific, with a clear CTA. Use Instantly or Lemlist to automate follow-ups.

Your goal is not a thousand users. It's **one paying customer.** That first dollar is the strongest form of validation that exists. It means a real person trusts your solution enough to trade money for it.

## After the Sprint: Iterate or Kill

One week after launch, you'll land in one of three places:

**You got paying customers.** You've validated the hypothesis. Talk to those customers. Understand *why* they bought. Double down on what's working. Your MoveFast admin dashboard, with built-in analytics and user impersonation, makes it easy to see exactly how people are using your product.

**You got interest but no money.** People said "this is cool" but didn't pay. Dig in. Is the price wrong? Is the core feature incomplete? Is the pain just not severe enough? Iterate on the product, not the infrastructure.

**Nobody cared.** No responses, no clicks, no engagement. This is actually the most valuable outcome: you just saved yourself months of building the wrong thing. Kill the project. Move on. With MoveFast, your next attempt starts from the same solid foundation without rebuilding from zero.

## The Math That Makes This Work

MoveFast costs [$90, a one-time payment](https://movefa.st) for lifetime access. Compare that to the alternative: ~$200 in LLM tokens and ~40 hours of your time building auth, payments, email, and database infrastructure from scratch. Every time you start a new project.

At $90, the boilerplate pays for itself the first time you use it. By your second or third idea, you're saving hundreds of dollars and dozens of hours *per attempt*.

And that's the real unlock. When spinning up a new SaaS costs you a weekend instead of a month, you can afford to run more experiments. More experiments mean more shots on goal. And more shots on goal mean you find the idea that actually works, faster.

**Ship fast. Fail cheap. When you hit gold, you'll already have the foundation to scale.**

# Mastering SaaS Pricing: Why You're Probably Undercharging

Source: https://movefa.st/blogs/mastering-saas-pricing
Published: 2026-05-10
Author: Kenil

Setting your price too low is the fastest way to kill your SaaS. Learn how to transition from cost-plus pricing to value-based pricing and instantly increase your ARR.

Most founders agonize over their tech stack, their landing page copy, their launch strategy. Then they slap a **$5/month** price tag on their product and wonder why they can't sustain the business.

Pricing is the single highest-leverage decision you'll make. Get it wrong and nothing else matters, not your features, not your marketing, not your code quality. Get it right and you buy yourself runway, focus, and the ability to actually serve your customers well.

I've thought about this a lot while building [MoveFast](https://movefa.st), and I want to break down the pricing strategies that actually work, using MoveFast's own pricing as a real-world case study.

## The Problem with Cheap SaaS

When you sell to businesses, price is a proxy for trust. If a company has a burning **$10,000 problem**, they won't trust a $5/month tool to solve it. Low prices signal that your product is a side project that might disappear next month.

But the damage goes deeper than perception. A low price point systematically destroys your unit economics:

| Metric | At $5/mo | At $99/mo |
|--------|----------|-----------|
| Annual revenue per customer | $60 | $1,188 |
| Customers needed for $100K ARR | 1,667 | 85 |
| Customer acquisition budget | ~$0 | ~$200 |
| Support capacity | Minimal | Dedicated |
| Sustainable growth | Unlikely | Very possible |

At $5/month, you can't afford paid ads, quality support, or sales reps. You're forcing yourself to rely entirely on viral, organic growth, which is incredibly rare for B2B tools.

## Cost-Plus vs. Value-Based Pricing

There are two fundamental approaches to pricing, and most founders default to the wrong one.

**Cost-plus pricing** is when you calculate your infrastructure costs, add a small margin, and charge that amount. *"My servers cost $2/user/month, so I'll charge $5."* This is terrible for software because the marginal cost of serving a new user is near zero. You're leaving enormous value on the table.

**Value-based pricing** is when you charge based on the value your customer **extracts** from your product. This requires understanding your customer's world: what are they spending time and money on today that your tool eliminates?

Here's a concrete example:

> If your tool saves a marketing agency 10 hours a week, and they bill at $100/hour, you're saving them **$4,000/month**. Charging $199/month for that isn't expensive. It's a **20x return on investment**.

The key insight: **your price should be anchored to customer value, not to your costs.** When there's a massive gap between value delivered and price charged, customers are happy, retention is high, and your business is sustainable.

## Case Study: How MoveFast Prices Itself

Let me pull back the curtain on how I priced [MoveFast](https://movefa.st), a production-ready Next.js SaaS boilerplate, because it illustrates several pricing principles in action.

**The price: $90 one-time payment. Lifetime access. All 18 stack variants included.**

Let's unpack the psychology behind each decision.

### One-Time vs. Subscription

For a boilerplate (something you buy once, clone, and build on top of) a one-time payment makes more sense than a subscription. The value is delivered upfront: you get the codebase, you ship your SaaS, you move on. Forcing a subscription on a product like this would feel misaligned with how customers actually use it.

But here's the thing: **when you're building your own SaaS product on top of MoveFast, subscriptions might be exactly right for you.** That's why MoveFast ships with built-in support for both one-time payments and subscriptions via `PAYMENT_MODEL` in your pricing config. Different products demand different pricing models, and your boilerplate should support whatever your business needs.

### Value-Based Justification

MoveFast saves you roughly **$200 in LLM costs** you'd otherwise spend prompting your way through boilerplate setup, plus approximately **40 hours of development time**. If you value your time at even $50/hour, that's $2,000 worth of saved effort.

$90 for $2,000+ in value? That's a **22x return.** The price isn't cheap because the product is unserious. It's cheap because the value gap is intentionally massive, which drives word-of-mouth and high conversion rates.

### Everything Included

With MoveFast, you don't pick a tier. You get everything in one purchase:

- All 18 stack variants (MongoDB/Supabase, Resend/Postmark/Nodemailer, Stripe/Polar/Dodo)
- Next.js 16 with React 19, TypeScript, Tailwind CSS 4, shadcn/ui
- Auth.js with magic links, Google OAuth, RBAC, impersonation
- Full admin dashboard with user management, analytics, waitlist, invites
- 7 React Email templates with local preview
- SEO, sitemap, robots.txt, blog system, docs system
- Lifetime updates and support

One price. No upsells. No feature gating.

## Making Pricing Experimentation Easy

One of the biggest mistakes founders make is treating pricing as a one-time decision. It's not. **Pricing is a continuous experiment.**

This is exactly why MoveFast ships with a flexible pricing configuration in `src/config/pricing.config.ts`. You can:

- Switch between `PaymentModel.ONE_TIME` and `PaymentModel.SUBSCRIPTION` with one line
- Define multiple pricing tiers with features, badges, and highlight states
- Set any currency (code and symbol separately)
- Add monthly/yearly billing toggles for subscription plans

The payment infrastructure (Stripe, Polar, or Dodo, depending on your variant) handles checkout, webhooks, subscription lifecycle, and billing portals. That means you can:

- Launch with a one-time lifetime deal (like MoveFast does)
- Switch to monthly subscriptions once you have recurring value to deliver
- Test annual vs. monthly pricing
- Add usage-based billing as you scale
- Run pricing experiments without rewriting your payment infrastructure

```typescript
// One line changes your entire payment model
export const PAYMENT_MODEL: PaymentModel = PaymentModel.ONE_TIME;
// or
export const PAYMENT_MODEL: PaymentModel = PaymentModel.SUBSCRIPTION;
```

The payment integration is not just a tutorial in a README. It's production-ready code with webhook verification, payment history tracking, normalized events across all three providers, and error handling already wired up.

## How to Structure Your Pricing Tiers

If you're building a SaaS with recurring revenue, a three-tier structure is a proven starting point:

### 1. The Starter Tier (The Anchor)

**Price:** $29-$49/month

This tier exists to capture early adopters and create a psychological anchor that makes your Pro tier look like the obvious upgrade. Include just enough features to be useful, but leave out what growing teams need: team seats, advanced analytics, priority support.

### 2. The Pro Tier (The Sweet Spot)

**Price:** $79-$149/month

This is your core revenue driver. It should include everything a serious team needs: multiple seats, priority support, advanced analytics, API access, and custom branding.

> **Pro tip:** Design your pricing page so the Pro tier is visually highlighted as the "recommended" option. MoveFast supports this with the `highlighted: true` and `badge: "Most Popular"` fields in the pricing config. Studies show this increases Pro tier selection by 20-30%.

### 3. The Enterprise Tier (The Cash Cow)

**Price:** Custom pricing (or $499+/month)

For large organizations that need SSO, custom SLAs, dedicated account management, and compliance features. Don't show a price. Use a "Contact Sales" button. This signals exclusivity and lets you price based on each customer's specific needs.

MoveFast's `PRICING_PLANS` array supports as many tiers as you want, each with its own product IDs, feature lists, CTAs, and visual styling:

```typescript
export const PRICING_PLANS: PricingPlan[] = [
    {
        name: "Starter",
        productIds: { monthly: "prod_starter_monthly", yearly: "prod_starter_yearly" },
        price: { monthly: 29, yearly: 23 },
        features: [
            { name: "Up to 5 team members", included: true },
            { name: "Advanced analytics", included: false },
        ],
        cta: { text: "Get Started", variant: "outline" },
    },
    {
        name: "Pro",
        productIds: { monthly: "prod_pro_monthly", yearly: "prod_pro_yearly" },
        price: { monthly: 79, yearly: 63 },
        highlighted: true,
        badge: "Most Popular",
        features: [
            { name: "Unlimited team members", included: true },
            { name: "Advanced analytics", included: true },
        ],
        cta: { text: "Start Free Trial", variant: "default" },
    },
];
```

## The Quarterly Pricing Experiment

Here's a simple framework you can run every quarter:

**Step 1: Double your price.** Yes, literally. Deploy the new pricing page and run it for 2 weeks.

**Step 2: Measure three metrics:**
- Conversion rate (visitors to signups)
- Revenue per visitor
- Customer quality (engagement, retention, support load)

**Step 3: Analyze.** In most cases, you'll find that conversion drops slightly (10-20%), but revenue per visitor jumps significantly (60-80%). Customer quality almost always improves: fewer tire-kickers, more serious users.

**Step 4: Adjust.** Find the sweet spot where revenue per visitor is maximized while maintaining a healthy conversion rate.

With MoveFast's payment infrastructure already wired up, running these experiments is simple. Update your pricing config, redeploy, and start collecting data. No payment infrastructure rewrites. No webhook debugging. Just ship and learn.

## The Takeaway

**Your price is a signal.** It tells the market how seriously you take your product, how much value you believe it delivers, and what kind of customer you're building for.

Don't set it based on server costs. Don't set it based on competitors. Set it based on the value you create, and make sure there's a massive gap between value delivered and price charged.

That gap is where happy customers, low churn, and sustainable growth live. And if you want to start testing your pricing strategy today, [MoveFast](https://movefa.st) gives you the payment infrastructure to do it (Stripe, Polar, or Dodo Payments, one-time or subscription) right out of the box for $90.

