backpine saas-kit

repository·main·Indexed 20 days ago

https://github.com/backpine/saas-kit

A monorepo providing a full-stack SaaS application architecture. It features a user-facing frontend built with TanStack Start on Cloudflare and a backend data service (Worker Publisher) designed to create and deploy Workers into a Cloudflare Dispatch Namespace using the Cloudflare SDK. The kit includes integration with Better Auth for authentication, Drizzle for database management, and TanStack Query for server function integration.

Tokens
18.4K
Snippets
62
Records
68
Agent score
70%

What's inside saas-kit

  1. Overview of the Worker Publisher service

    main

    The data-service (Worker Publisher) is a Cloudflare Worker designed to create and deploy other Workers into a Dispatch Namespace using the Cloudflare SDK.

    Key behaviors:

    • It automatically creates a Workers for Platforms dispatch namespace.
    • It uses the Cloudflare SDK to deploy Workers into that namespace.
    • Each deployed Worker is accessible via its own path: /{worker-name}.
    • The main Worker functions as a router, forwarding incoming requests to the appropriate deployed Worker.
    • Every deployed Worker operates within its own isolated environment.
  2. Compare Client-side vs Server-side route protection

    main

    When choosing how to protect routes in your application, consider the following trade-offs:

    FeatureClient-side ProtectionServer-side (SSR) Protection
    Validation LocationBrowserServer
    User ExperienceShows loading states while checkingImmediate auth decision (no loading flicker)
    Security/SEOLower (content is checked after mount)Higher (content is validated before delivery)
    ComplexityRequires handling isPending statesRequires beforeLoad and error handling

    Note: For routes defined under a layout like /_authed, all child routes will automatically inherit the protection logic defined in that layout route.

  3. Manage application features via Polar Product Metadata

    main

    Instead of maintaining a local database for subscription tiers, use Polar's product metadata to control feature access. You can define a JSON object in the Polar dashboard for each product containing features and limits.

    Example Metadata Schema:

    {
      "features": {
        "analytics": true,
        "api_access": true,
        "priority_support": true,
        "custom_branding": false
      },
      "limits": {
        "projects": 10,
        "storage_gb": 100,
        "api_calls_per_month": 10000
      }
    }

    This allows for real-time feature updates without code deployments.

  4. Manage routes with TanStack Router file-based routing

    main

    Routes are automatically generated from files in the ./src/routes directory.

    Adding a Route

    Add a new file to ./src/routes. TanStack will automatically generate the route content.

    Using Layouts

    The root layout is located in src/routes/__root.tsx. Use the <Outlet /> component to render the child route content. Anything added to the root route will appear in all routes.

    Use the Link component from @tanstack/react-router for SPA navigation.

    import { Outlet, createRootRoute } from '@tanstack/react-router'
    import { Link } from "@tanstack/react-router";
    
    export const Route = createRootRoute({
      component: () => (
        <>
          <header>
            <nav>
              <Link to="/">Home</Link>
              <Link to="/about">About</Link>
            </nav>
          </header>
          <Outlet />
        </>
      ),
    })
  5. Initialize Cloudflare D1 runtime client

    main

    For Cloudflare D1, the database is initialized using the D1 binding provided by the Cloudflare environment. This is done in the src/server.ts entry point.

    ### Cloudflare D1 Runtime Setup
    // packages/data-ops/database/setup.ts
    import { drizzle } from "drizzle-orm/d1";
    
    let db: ReturnType<typeof drizzle>;
    
    export function initDatabase(d1Db: D1Database) {
      if (db) {
        return db
      }
      db = drizzle(d1Db);
      return db;
    }
    
    export function getDb() {
      if (!db) {
        throw new Error("Database not initialized");
      }
      return db;
    }
    
    // src/server.ts - TanStack Start Server Entry
    import { initDatabase } from "@repo/data-ops/database/setup";
    import handler from "@tanstack/react-start/server-entry";
    import { env } from "cloudflare:workers";
    
    export default {
      fetch(request: Request) {
        const db = initDatabase(env.DB); // D1 binding
    
        return handler.fetch(request, {
          context: {
            fromFetch: true,
          },
        });
      },
    };
  6. Quick Start with TanStack Start on Cloudflare

    main

    To get started with the application, install dependencies, run the development server, build for production, or deploy to Cloudflare using the following commands:

    # Install dependencies
    pnpm install
    
    # Start development server
    pnpm dev
    
    # Build for production
    pnpm build
    
    # Deploy to Cloudflare
    pnpm deploy
  7. Create database queries in @repo/data-ops

    main

    Organize database queries within the packages/data-ops/src/queries/ directory. Group related operations into files (e.g., polar.ts for subscription management).

    All queries must use the getDb() function from @/database/setup to ensure consistent connection management in serverless environments. Use Drizzle ORM for type-safe queries and follow best practices by including explicit TypeScript types for all parameters and return values.

    // packages/data-ops/src/queries/polar.ts
    import { getDb } from "@/database/setup";
    import { subscriptions } from "@/drizzle/schema";
    import { eq } from "drizzle-orm";
    
    export async function updateSubscription(data: { ... }) {
      const db = getDb();
      await db
        .insert(subscriptions)
        .values({ ... })
        .onConflictDoUpdate({
          target: [subscriptions.userId],
          set: { ... },
        });
    }
    
    export async function getSubscription(userId: string) {
      const db = getDb();
      const subscription = await db
        .select()
        .from(subscriptions)
        .where(eq(subscriptions.userId, userId));
      return subscription;
    }
  8. Install @repo/data-ops in an application

    main

    Add @repo/data-ops as a dependency in your application's package.json using the workspace:^ syntax. This ensures the application uses the local version of the package from the monorepo workspace, allowing you to use the latest queries immediately.

    // apps/user-application/package.json
    {
      "name": "user-application",
      "dependencies": {
        "@repo/data-ops": "workspace:^"
      }
    }
  9. Generate Better Auth database schemas and migrations

    main

    Use the following command sequence to create your authentication tables and migrations:

    1. Generate schemas: Run pnpm run better-auth:generate to create packages/data-ops/src/drizzle/auth-schema.ts.
    2. Generate migrations: Run pnpm run drizzle:generate to create SQL migration files in packages/data-ops/src/drizzle.
    3. Apply migrations (optional): Run pnpm run drizzle:migrate to apply the changes to your database.

    Note: Auth tables are prefixed with auth_ and are filtered out in drizzle.config.ts to prevent conflicts during drizzle:pull operations.

    pnpm run better-auth:generate
    pnpm run drizzle:generate
    pnpm run drizzle:migrate
  10. Configure Drizzle Kit for schema management

    main

    Drizzle Kit is used in the packages/data-ops package to manage schemas and generate TypeScript types. You can pull existing database schemas into your project by running pnpm run drizzle:pull. The generated schemas will be available in src/drizzle/schema.ts.

    ### PostgreSQL Drizzle Configuration
    // packages/data-ops/drizzle.config.ts
    import type { Config } from "drizzle-kit";
    const config: Config = {
      out: "./src/drizzle",
      schema: ["./src/drizzle/auth-schema.ts"],
      dialect: "postgresql",
      dbCredentials: {
        url: `postgresql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}`,
      },
      tablesFilter: ["!_cf_KV", "!auth_*"],
    };
    
    export default config satisfies Config;
    
    ### MySQL Drizzle Configuration
    // packages/data-ops/drizzle.config.ts
    import type { Config } from "drizzle-kit";
    const config: Config = {
      out: "./src/drizzle",
      schema: ["./src/drizzle/auth-schema.ts"],
      dialect: "mysql",
      dbCredentials: {
        url: `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}`,
      },
      tablesFilter: ["!_cf_KV", "!auth_*"],
    };
    
    export default config satisfies Config;
    
    ### Cloudflare D1 Drizzle Configuration
    // packages/data-ops/drizzle.config.ts
    import type { Config } from "drizzle-kit";
    const config: Config = {
      out: "./src/drizzle",
      schema: ["./src/drizzle/auth-schema.ts"],
      dialect: "sqlite",
      driver: "d1-http",
      dbCredentials: {
        accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
        databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
        token: process.env.CLOUDFLARE_D1_TOKEN!,
      },
      tablesFilter: ["!_cf_KV", "!auth_*"],
    };
    
    export default config satisfies Config;
  11. Configure Better Auth environment variables

    main

    Set up the required environment variables in packages/data-ops/.env. You need a BETTER_AUTH_SECRET for token signing. If using Google OAuth, also provide GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.

    To generate a secure secret, use: openssl rand -base64 32

    OAuth Redirect URI: When configuring your OAuth provider (e.g., Google Cloud Console), add the following redirect URI: https://your-domain.com/api/auth/callback/google

    # packages/data-ops/.env
    BETTER_AUTH_SECRET="your-secret-key-here"
    
    # Google OAuth (optional)
    GOOGLE_CLIENT_ID="your-google-client-id"
    GOOGLE_CLIENT_SECRET="your-google-client-secret"