SaaS Template by Adrian Hajdin

repository·main·Indexed 41 days ago

https://github.com/adrianhajdin/saas-template

A high-powered SaaS application template for building Learning Management Systems (LMS) and similar platforms. Built with Next.js, Clerk for authentication and billing, Supabase for PostgreSQL database and storage, shadcn/ui, Tailwind CSS, and TypeScript. Includes pre-integrated layers for user management, recipe-based content unlocking with billing plan limits, and server actions for data manipulation.

Tokens
2.8K
Snippets
13
Records
14
Agent score
52%

What's inside SaaS Template

  1. Overview of the SaaS Template Tech Stack

    main

    This template is built using a modern full-stack architecture designed for rapid SaaS deployment:

    • Next.js: React framework for server-side rendering and API routes.
    • Clerk: Handles authentication, user management, and subscription-based access control.
    • Supabase: Provides the PostgreSQL database, real-time capabilities, and storage.
    • shadcn/ui: Accessible component library built on Radix UI and Tailwind CSS.
    • Tailwind CSS: Utility-first CSS for styling.
    • TypeScript: Static typing for improved code quality.
    • Zod: Schema validation for data integrity.
  2. Quick Start Guide for SaaS Template

    main

    Follow these steps to set up the SaaS template locally.

    Prerequisites

    • Git
    • Node.js
    • npm

    Installation Steps

    1. Clone the repository:
      git clone https://github.com/JavaScript-Mastery-Pro/saas-template.git
      cd saas-template
    2. Install dependencies:
      npm install
    3. Configure Environment Variables: Create a .env file in the root directory and populate it with your Clerk and Supabase credentials (see Environment Variables).
    4. Run the development server:
      npm run dev
      The application will be available at http://localhost:3000.
    git clone https://github.com/JavaScript-Mastery-Pro/saas-template.git
    cd saas-template
    npm install
    npm run dev
  3. Set Up Environment Variables

    main

    Create a .env file in the root of your project. You must provide valid credentials from Clerk and Supabase to enable authentication and database functionality.

    # Clerk
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
    CLERK_SECRET_KEY=
    NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
    NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
    NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
    
    # Supabase
    NEXT_PUBLIC_SUPABASE_URL=
    NEXT_PUBLIC_SUPABASE_ANON_KEY=
  4. Configure ESLint with Next.js and TypeScript presets

    main

    This project uses the ESLint Flat Config format (eslint.config.mjs) and utilizes @eslint/eslintrc's FlatCompat utility to maintain compatibility with legacy Next.js configuration presets. The configuration extends the following rulesets:

    • next/core-web-vitals: Next.js specific rules for optimizing Core Web Vitals.
    • next/typescript: Next.js specific rules for TypeScript integration.

    To use this configuration in your project, ensure you have the necessary Next.js and TypeScript ESLint plugins installed.

    import { dirname } from "path";
    import { fileURLToPath } from "url";
    import { FlatCompat } from "@eslint/eslintrc";
    
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    
    const compat = new FlatCompat({
      baseDirectory: __dirname,
    });
    
    const eslintConfig = [
      ...compat.extends("next/core-web-vitals", "next/typescript"),
    ];
    
    export default eslintConfig;
  5. Retrieve recipe comments with user details using getRecipeComments

    main

    Use the getRecipeComments server action to fetch all comments associated with a specific recipeId.

    This function performs the following steps:

    1. Fetches all rows from the Supabase comments table where recipe_id matches the provided ID, ordered by created_at descending.
    2. Retrieves the corresponding user profiles (first name and image URL) from Clerk using the user_id found in the comments.
    3. Returns an array of comment objects enriched with userFirstName and userImageUrl properties.

    Returns: An array of objects containing the original comment data plus user metadata.

    import { getRecipeComments } from '@/lib/actions/comment.actions';
    
    // Usage to fetch comments for a recipe page
    const comments = await getRecipeComments("recipe-uuid-here");
    
    // Each comment object in the array will have:
    // { ..., userFirstName: string, userImageUrl: string }
  6. Fetch all recipes with user details

    main

    Use getRecipes to retrieve a list of all recipes in the database. This function automatically enriches each recipe with the author's Clerk user details (userFirstName, userImageUrl) and determines if the recipe is unlocked for the current user. A recipe is considered unlocked if the user is the author or if they have explicitly unlocked it via the recipes_unlocked table.

    const recipes = await getRecipes();
    // Each recipe object includes:
    // id, name, ingredients, user_id, userFirstName, userImageUrl, unlocked
  7. Unlock a recipe for the current user

    main

    Use unlockRecipe(recipeId: string) to grant the current user access to a specific recipe.

    Logic and Limits:

    • If the user is not signed in, they are redirected to /sign-in.
    • The function checks the user's Clerk Billing Plan features to enforce unlock limits:
      • recipe_limit_unlimited: No limit.
      • recipe_limit_100: Maximum 100 unlocks.
      • recipe_limit_3: Maximum 3 unlocks.
      • Default: 1 unlock.
    • If the limit is reached, it returns { success: false, message: "limit reached" }.
    • On success, it returns { success: true, message: "recipe unlocked" }.
    const result = await unlockRecipe("recipe-id");
    
    if (result.success) {
      console.log("Unlocked!");
    } else {
      console.error(result.message); // e.g., "limit reached"
    }
  8. Get a single recipe by ID

    main

    Use getRecipe(id: string) to fetch a specific recipe.

    Access Rules:

    • If the current user is the author of the recipe, the data is returned immediately.
    • If the user is not the author, the function checks if the recipe has been unlocked for them. If not, it returns null.
    • If the user is not authenticated, it throws an "Unauthorized" error.
    try {
      const recipe = await getRecipe("recipe-id-here");
      if (!recipe) {
        // Handle case where recipe is not unlocked or doesn't exist
      }
    } catch (error) {
      // Handle unauthorized error
    }
  9. Initialize a Supabase client with Clerk authentication

    main

    Use createSupabaseClient to instantiate a Supabase client that is automatically integrated with Clerk authentication. This function retrieves the Clerk session token via auth().getToken() and passes it as an accessToken to the Supabase client, allowing you to perform authenticated requests against your Supabase database using the user's current Clerk session.

    To use this, ensure the following environment variables are set in your project:

    • NEXT_PUBLIC_SUPABASE_URL
    • NEXT_PUBLIC_SUPABASE_ANON_KEY
    import { createSupabaseClient } from '@/lib/supabase';
    
    const supabase = createSupabaseClient();
    
    // Example: Fetching data with the authenticated client
    const { data, error } = await supabase.from('your_table').select('*');
  10. Create a new recipe

    main

    Use createRecipe(recipe: Recipe) to insert a new recipe into the database. The user_id is automatically assigned to the currently authenticated user. This action requires the user to be signed in; otherwise, it throws an "Unauthorized" error.

    import { Recipe } from "@/types";
    
    const newRecipe: Recipe = {
      name: "Pasta",
      ingredients: ["Flour", "Eggs"],
      instructions: "Mix and boil.",
      // user_id is handled by the server action
    };
    
    const createdRecipe = await createRecipe(newRecipe);