### Setup Local Development with npm Source: https://github.com/criticalmoments/cmsaasstarter/blob/main/README.md Commands to clone the repository, install dependencies, create an environment file, and run the project locally in development mode. Assumes Node.js and npm are installed. ```bash git pull [Your Repo Created Above] cd CMSaasStarter ## or your repo name if different npm install ## Create an env file. You'll replace the values in this in later steps. cp .env.example .env.local ## Run the project locally in dev mode, and launch the browser npm run dev -- --open ``` -------------------------------- ### Environment Variables Setup (Bash) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Defines the required environment variables for the application, including Supabase URL, keys, Stripe API key, and optional email configuration. These variables are crucial for connecting to external services and enabling specific functionalities. ```bash # .env.local PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_SUPABASE_ANON_KEY=your-anon-key PRIVATE_SUPABASE_SERVICE_ROLE=your-service-role-secret PRIVATE_STRIPE_API_KEY=sk_test_your_stripe_key # Optional: Email configuration PRIVATE_RESEND_API_KEY=re_your_resend_key PRIVATE_ADMIN_EMAIL=admin@yourcompany.com PRIVATE_FROM_ADMIN_EMAIL=noreply@yourcompany.com ``` -------------------------------- ### Build Site Search Index with Fuse.js Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Constructs a Fuse.js search index from pre-rendered HTML pages at build time. It reads HTML files, extracts text content, titles, and descriptions, and then creates a searchable index. ```typescript // src/lib/build_index.ts import { glob } from "glob" import { convert } from "html-to-text" import JSDOM from "jsdom" import Fuse from "fuse.js" const excludePaths = ["/search"] export async function buildSearchIndex() { const indexData = [] const pagesPath = path.join(".", ".svelte-kit/output/prerendered/pages") const allFiles = glob.sync(path.join(pagesPath, "**/*.html")) for (const file of allFiles) { const webPath = file .replace(pagesPath, "") .replace("/index.html", "") .replace(".html", "") if (excludePaths.includes(webPath)) continue const data = fs.readFileSync(file, "utf8") const plaintext = convert(data, { selectors: [ { selector: "a", options: { ignoreHref: true, linkBrackets: false } }, { selector: "img", format: "skip" }, ], }) const dom = new JSDOM.JSDOM(data) const title = dom.window.document.querySelector("title")?.textContent || "Page" const description = dom.window.document .querySelector('meta[name="description"]') ?.getAttribute("content") || "" indexData.push({ title, description, body: plaintext, path: webPath }) } const index = Fuse.createIndex(["title", "description", "body"], indexData) return { index: index.toJSON(), indexData, buildTime: Date.now() } } ``` -------------------------------- ### Stripe Pricing Plans Configuration (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Defines the pricing structure for subscription tiers, including free, pro, and enterprise plans. Each plan includes an ID, name, description, price, interval, and associated Stripe product and price IDs for integration with Stripe. ```typescript // src/routes/(marketing)/pricing/pricing_plans.ts export const defaultPlanId = "free" export const pricingPlans = [ { id: "free", name: "Free", description: "A free plan to get you started!", price: "$0", priceIntervalName: "per month", stripe_price_id: null, features: ["MIT Licence", "Fast Performance", "Stripe Integration"], }, { id: "pro", name: "Pro", description: "Professional features for growing teams", price: "$5", priceIntervalName: "per month", stripe_price_id: "price_1NkdZCHMjzZ8mGZnRSjUm4yA", stripe_product_id: "prod_OXj1CcemGMWOlU", features: ["Everything in Free", "Priority support", "Advanced features"], }, { id: "enterprise", name: "Enterprise", description: "Full-featured plan for large organizations", price: "$15", priceIntervalName: "per month", stripe_price_id: "price_1Nkda2HMjzZ8mGZn4sKvbDAV", stripe_product_id: "prod_OXj20YNpHYOXi7", features: ["Everything in Pro", "Custom integrations", "SLA guarantee"], }, ] ``` -------------------------------- ### Configure Blog Posts with Metadata Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Defines blog post structure and metadata for SEO and RSS generation. It includes a TypeScript type for blog posts and an array of post objects, which are then parsed and sorted by date. ```typescript // src/routes/(marketing)/blog/posts.ts export const blogInfo = { name: "SaaS Starter Blog", description: "A sample blog", } export type BlogPost = { link: string date: string // 'YYYY-MM-DD' title: string description: string parsedDate?: Date } const blogPosts: BlogPost[] = [ { title: "How we built a beautiful 41kb SaaS website", description: "How to use this template to bootstrap your own site.", link: "/blog/how_we_built_our_41kb_saas_website", date: "2024-03-10", }, { title: "Example Blog Post", description: "A sample blog post demonstrating the blog engine", link: "/blog/example_blog_post", date: "2023-03-13", }, ] // Parse and sort posts by date for (const post of blogPosts) { const [year, month, day] = post.date.split("-").map(Number) post.parsedDate = new Date(year, month - 1, day) } export const sortedBlogPosts = blogPosts.sort( (a, b) => (b.parsedDate?.getTime() ?? 0) - (a.parsedDate?.getTime() ?? 0) ) ``` -------------------------------- ### Site Metadata Configuration (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Configures essential site metadata for SEO and branding, including website name, base URL, and description. These constants are used throughout the application for consistent presentation and search engine optimization. ```typescript // src/config.ts export const WebsiteName: string = "SaaS Starter" export const WebsiteBaseUrl: string = "https://saasstarter.work" export const WebsiteDescription: string = "Open source, fast, and free to host SaaS template. Built with SvelteKit, Supabase, Stripe, Tailwind, DaisyUI, and Postgres" export const CreateProfileStep: boolean = true ``` -------------------------------- ### Create Stripe Checkout Session for Subscription (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt This function initiates a Stripe Checkout session for a user to purchase a subscription. It first ensures a Stripe customer exists and checks for existing subscriptions. If the user is eligible, it redirects them to Stripe Checkout. Dependencies include `getOrCreateCustomerId`, `fetchSubscription`, and the Stripe SDK. ```typescript // src/routes/(admin)/account/subscribe/[slug]/+page.server.ts export const load: PageServerLoad = async ({ params, url, locals: { safeGetSession, supabaseServiceRole }, }) => { const { session, user } = await safeGetSession() if (!session) redirect(303, "/login") if (params.slug === "free_plan") redirect(303, "/account") const { customerId } = await getOrCreateCustomerId({ supabaseServiceRole, user }) const { primarySubscription } = await fetchSubscription({ customerId }) if (primarySubscription) redirect(303, "/account/billing") const stripeSession = await stripe.checkout.sessions.create({ line_items: [{ price: params.slug, quantity: 1 }], customer: customerId, mode: "subscription", success_url: `${url.origin}/account`, cancel_url: `${url.origin}/account/billing`, }) redirect(303, stripeSession.url ?? "/pricing") } ``` -------------------------------- ### PostgreSQL Database Schema for Supabase Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Defines a PostgreSQL schema for Supabase, including tables for user profiles, Stripe customers, and contact requests. It also implements Row Level Security (RLS) policies and a trigger function to auto-create user profiles upon signup. ```sql -- database_migration.sql -- User profiles table CREATE TABLE profiles ( id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL PRIMARY KEY, updated_at TIMESTAMP WITH TIME ZONE, full_name TEXT, company_name TEXT, avatar_url TEXT, website TEXT, unsubscribed BOOLEAN NOT NULL DEFAULT FALSE ); ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY "Profiles are viewable by self." ON profiles FOR SELECT USING (auth.uid() = id); CREATE POLICY "Users can insert their own profile." ON profiles FOR INSERT WITH CHECK (auth.uid() = id); CREATE POLICY "Users can update own profile." ON profiles FOR UPDATE USING (auth.uid() = id); -- Stripe customers table CREATE TABLE stripe_customers ( user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL PRIMARY KEY, updated_at TIMESTAMP WITH TIME ZONE, stripe_customer_id TEXT UNIQUE ); ALTER TABLE stripe_customers ENABLE ROW LEVEL SECURITY; -- Contact requests table CREATE TABLE contact_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), updated_at TIMESTAMP WITH TIME ZONE, first_name TEXT, last_name TEXT, email TEXT, phone TEXT, company_name TEXT, message_body TEXT ); ALTER TABLE contact_requests ENABLE ROW LEVEL SECURITY; -- Auto-create profile on user signup CREATE FUNCTION public.handle_new_user() RETURNS TRIGGER AS $$ BEGIN INSERT INTO public.profiles (id, full_name, avatar_url) VALUES (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url'); RETURN new; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user(); ``` -------------------------------- ### Create Stripe Billing Portal Session (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt This function generates a URL for the Stripe Billing Portal, allowing users to manage their subscriptions. It ensures a Stripe customer exists before creating the portal session. Dependencies include `getOrCreateCustomerId` and the Stripe SDK. ```typescript // src/routes/(admin)/account/(menu)/billing/manage/+page.server.ts export const load: PageServerLoad = async ({ url, locals: { safeGetSession, supabaseServiceRole }, }) => { const { session, user } = await safeGetSession() if (!session) redirect(303, "/login") const { customerId } = await getOrCreateCustomerId({ supabaseServiceRole, user }) const portalSession = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: `${url.origin}/account/billing`, }) redirect(303, portalSession.url ?? "/account/billing") } ``` -------------------------------- ### Update User Profile Server Action (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Handles the creation or update of a user's profile information. It includes form data retrieval, validation for required fields, and upserts the profile data into the 'profiles' table in Supabase. Requires a valid Supabase session and user. ```typescript // src/routes/(admin)/account/api/+page.server.ts export const actions = { updateProfile: async ({ request, locals: { supabase, safeGetSession } }) => { const { session, user } = await safeGetSession() if (!session || !user?.id) redirect(303, "/login") const formData = await request.formData() const fullName = formData.get("fullName") as string const companyName = formData.get("companyName") as string const website = formData.get("website") as string // Validation const errorFields = [] if (!fullName) errorFields.push("fullName") if (!companyName) errorFields.push("companyName") if (!website) errorFields.push("website") if (errorFields.length > 0) { return fail(400, { errorMessage: "All fields required", errorFields }) } // Upsert profile const { error } = await supabase.from("profiles").upsert({ id: user.id, full_name: fullName, company_name: companyName, website: website, updated_at: new Date(), }) if (error) return fail(500, { errorMessage: "Unknown error" }) return { fullName, companyName, website } }, } ``` -------------------------------- ### Run Developer Tools Locally Source: https://github.com/criticalmoments/cmsaasstarter/blob/main/README.md Shell command to execute local developer tools scripts for checks, linting, and formatting. Requires making the script executable first. ```bash # first time only: chmod +x ./checks.sh ./checks.sh ``` -------------------------------- ### Manage Stripe Customer Creation and Caching (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt This function ensures a Stripe customer exists for a given user, creating one if necessary and caching the customer ID in the database. It utilizes Supabase for database operations and the Stripe API for customer management. Dependencies include Supabase client and Stripe SDK. ```typescript // src/routes/(admin)/account/subscription_helpers.server.ts import Stripe from "stripe" const stripe = new Stripe(PRIVATE_STRIPE_API_KEY, { apiVersion: "2023-08-16" }) export const getOrCreateCustomerId = async ({ supabaseServiceRole, user, }: { supabaseServiceRole: SupabaseClient user: User }) => { // Check for existing customer const { data: dbCustomer } = await supabaseServiceRole .from("stripe_customers") .select("stripe_customer_id") .eq("user_id", user.id) .single() if (dbCustomer?.stripe_customer_id) { return { customerId: dbCustomer.stripe_customer_id } } // Fetch profile for customer metadata const { data: profile } = await supabaseServiceRole .from("profiles") .select(`full_name, website, company_name`) .eq("id", user.id) .single() // Create Stripe customer const customer = await stripe.customers.create({ email: user.email, name: profile.full_name ?? "", metadata: { user_id: user.id, company_name: profile.company_name ?? "", website: profile.website ?? "", }, }) // Cache customer ID await supabaseServiceRole.from("stripe_customers").insert({ user_id: user.id, stripe_customer_id: customer.id, updated_at: new Date(), }) return { customerId: customer.id } } ``` -------------------------------- ### Supabase Authentication Hook (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Implements a server hook for SvelteKit to initialize Supabase clients and manage user sessions with JWT validation. It creates authenticated and service role clients, and provides a safe method to retrieve session data, including user information and multi-factor authentication status. ```typescript // src/hooks.server.ts import { createServerClient } from "@supabase/ssr" import { createClient, type AMREntry } from "@supabase/supabase-js" import type { Handle } from "@sveltejs/kit" import { sequence } from "@sveltejs/kit/hooks" export const supabase: Handle = async ({ event, resolve }) => { // Create authenticated client for user requests event.locals.supabase = createServerClient( PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => { event.cookies.set(name, value, { ...options, path: "/" }) }) }, }, } ) // Create service role client for admin operations event.locals.supabaseServiceRole = createClient( PUBLIC_SUPABASE_URL, PRIVATE_SUPABASE_SERVICE_ROLE, { auth: { persistSession: false } } ) // Safe session getter with JWT validation event.locals.safeGetSession = async () => { const { data: { session } } = await event.locals.supabase.auth.getSession() if (!session) return { session: null, user: null, amr: null } const { data: { user }, error: userError } = await event.locals.supabase.auth.getUser() if (userError) return { session: null, user: null, amr: null } const { data: aal } = await event.locals.supabase.auth.mfa.getAuthenticatorAssuranceLevel() return { session, user, amr: aal?.currentAuthenticationMethods as AMREntry[] } } return resolve(event) } export const handle: Handle = sequence(supabase, authGuard) ``` -------------------------------- ### OAuth Provider Configuration (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Defines the OAuth providers supported for authentication and styles the authentication UI using Supabase's shared appearance settings. This configuration allows for easy integration of social logins and customization of the login form's look and feel. ```typescript // src/routes/(marketing)/login/login_config.ts import { ThemeSupa } from "@supabase/auth-ui-shared" import type { Provider } from "@supabase/supabase-js" export const oauthProviders = ["github"] as Provider[] export const sharedAppearance = { theme: ThemeSupa, variables: { default: { colors: { brand: "oklch(var(--p))", brandAccent: "oklch(var(--ac))", inputText: "oklch(var(--n))", brandButtonText: "oklch(var(--pc))", }, fontSizes: { baseInputSize: "16px" }, }, }, className: { button: "authBtn" }, } ``` -------------------------------- ### Fetch User Subscription Status (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt This function retrieves a user's active subscription status from Stripe by querying their subscriptions and matching them with internal pricing plans. It returns the primary active subscription details and a flag indicating if the user has ever had a subscription. Dependencies include the Stripe SDK and a predefined `pricingPlans` array. ```typescript // src/routes/(admin)/account/subscription_helpers.server.ts export const fetchSubscription = async ({ customerId }: { customerId: string }) => { const stripeSubscriptions = await stripe.subscriptions.list({ customer: customerId, limit: 100, status: "all", }) // Find active subscription const primaryStripeSubscription = stripeSubscriptions.data.find((x) => { return x.status === "active" || x.status === "trialing" || x.status === "past_due" }) let appSubscription = null if (primaryStripeSubscription) { const productId = primaryStripeSubscription?.items?.data?.[0]?.price.product ?? "" appSubscription = pricingPlans.find((x) => x.stripe_product_id === productId) } return { primarySubscription: primaryStripeSubscription && appSubscription ? { stripeSubscription: primaryStripeSubscription, appSubscription } : null, hasEverHadSubscription: stripeSubscriptions.data.length > 0, } } ``` -------------------------------- ### Delete Account Server Action (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Provides functionality to permanently delete a user's account. It first verifies the user's current password and then uses the Supabase service role to delete the user from authentication and associated data. The user is signed out upon successful deletion. ```typescript // src/routes/(admin)/account/api/+page.server.ts export const actions = { deleteAccount: async ({ request, locals: { supabase, supabaseServiceRole, safeGetSession }, }) => { const { session, user } = await safeGetSession() if (!session || !user?.id) redirect(303, "/login") const formData = await request.formData() const currentPassword = formData.get("currentPassword") as string // Verify password const { error: pwError } = await supabase.auth.signInWithPassword({ email: user?.email || "", password: currentPassword, }) if (pwError) redirect(303, "/login/current_password_error") // Delete user with service role const { error } = await supabaseServiceRole.auth.admin.deleteUser(user.id, true) if (error) return fail(500, { errorMessage: "Unknown error" }) await supabase.auth.signOut() redirect(303, "/") }, } ``` -------------------------------- ### Send User Email with Verification and Unsubscribe Checks (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Sends templated emails to users after verifying their email confirmation status and checking their unsubscribe preferences. It utilizes Supabase for user data retrieval and a generic `sendTemplatedEmail` function for the actual email dispatch. Inputs include user details, subject, sender email, template name, and template properties. It returns early if the user's email is not verified or if they are unsubscribed. ```typescript // src/lib/mailer.ts export const sendUserEmail = async ({ user, subject, from_email, template_name, template_properties, }: { user: User subject: string from_email: string template_name: string template_properties: Record }) => { if (!user.email) return // Verify email is confirmed const { data: serviceUserData } = await serverSupabase.auth.admin.getUserById(user.id) const emailVerified = serviceUserData.user?.email_confirmed_at || serviceUserData.user?.user_metadata?.email_verified if (!emailVerified) return // Check unsubscribe status const { data: profile } = await serverSupabase .from("profiles") .select("unsubscribed") .eq("id", user.id) .single() if (profile?.unsubscribed) return await sendTemplatedEmail({ subject, to_emails: [user.email], from_email, template_name, template_properties, }) } // Usage: Send welcome email await sendUserEmail({ user: session.user, subject: "Welcome!", from_email: "no-reply@yourcompany.com", template_name: "welcome_email", template_properties: { companyName: "SaaS Starter", WebsiteBaseUrl: "https://saasstarter.work", }, }) ``` -------------------------------- ### Send Templated Emails using Handlebars (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Sends emails using Handlebars templates for both HTML and plaintext content. It dynamically loads template files based on the provided `template_name` and compiles them with the given `template_properties`. Requires `PRIVATE_RESEND_API_KEY` environment variable and the `resend` package. It handles potential errors during template loading and compilation, sending the email via Resend if either plaintext or HTML content is successfully generated. ```typescript // src/lib/mailer.ts import handlebars from "handlebars" export const sendTemplatedEmail = async ({ subject, to_emails, from_email, template_name, template_properties, }: { subject: string to_emails: string[] from_email: string template_name: string template_properties: Record }) => { if (!env.PRIVATE_RESEND_API_KEY) return // Load and compile plaintext template let plaintextBody: string | undefined try { const textTemplate = await import(`./emails/${template_name}_text.hbs?raw`) plaintextBody = handlebars.compile(textTemplate.default)(template_properties) } catch { /* Optional */ } // Load and compile HTML template let htmlBody: string | undefined try { const htmlTemplate = await import(`./emails/${template_name}_html.hbs?raw`) htmlBody = handlebars.compile(htmlTemplate.default)(template_properties) } catch { /* Optional */ } if (!plaintextBody && !htmlBody) return const resend = new Resend(env.PRIVATE_RESEND_API_KEY) await resend.emails.send({ from: from_email, to: to_emails, subject, ...(plaintextBody && { text: plaintextBody }), ...(htmlBody && { html: htmlBody }), }) } ``` -------------------------------- ### Send Admin Notification Email (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt This function sends notification emails to a predefined admin email address using the Resend service. It requires Resend API key and optionally a custom 'from' email address. The email content includes a subject and body. Dependencies include the Resend SDK and environment variables for API keys and email addresses. ```typescript // src/lib/mailer.ts import { Resend } from "resend" export const sendAdminEmail = async ({ subject, body, }: { subject: string body: string }) => { if (!env.PRIVATE_ADMIN_EMAIL) return const resend = new Resend(env.PRIVATE_RESEND_API_KEY) await resend.emails.send({ from: env.PRIVATE_FROM_ADMIN_EMAIL || env.PRIVATE_ADMIN_EMAIL, to: [env.PRIVATE_ADMIN_EMAIL], subject: "ADMIN_MAIL: " + subject, text: body, }) } // Usage example: await sendAdminEmail({ subject: "New contact request", body: `New contact from ${firstName} ${lastName}.\nEmail: ${email}\nMessage: ${message}`, }) ``` -------------------------------- ### Run Developer Tools with Git Hooks Source: https://github.com/criticalmoments/cmsaasstarter/blob/main/README.md This script acts as a pre-commit Git hook to automatically run standard checks before allowing a commit. It navigates to the script's directory and executes a checks.sh script located two directories up. Ensure the script is executable. ```shell #!/bin/sh # Run standard checks before committing cd "$(dirname "$0")" sh ../../checks.sh ``` -------------------------------- ### Update Password Server Action (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt Manages user password updates, supporting recovery sessions. It validates new passwords, checks against the current password (unless in a recovery session), and updates the user's password in Supabase Auth. Requires a valid Supabase session. ```typescript // src/routes/(admin)/account/api/+page.server.ts export const actions = { updatePassword: async ({ request, locals: { supabase, safeGetSession } }) => { const { session, user, amr } = await safeGetSession() if (!session) redirect(303, "/login") const formData = await request.formData() const newPassword1 = formData.get("newPassword1") as string const newPassword2 = formData.get("newPassword2") as string const currentPassword = formData.get("currentPassword") as string // Check for password recovery session const recoveryAmr = amr?.find((x) => x.method === "recovery") const isRecoverySession = recoveryAmr && !currentPassword // Validation if (newPassword1.length < 6) { return fail(400, { errorMessage: "Password must be at least 6 characters" }) } if (newPassword1 !== newPassword2) { return fail(400, { errorMessage: "Passwords don't match" }) } // Verify current password if not recovery session if (!isRecoverySession) { const { error } = await supabase.auth.signInWithPassword({ email: user?.email || "", password: currentPassword, }) if (error) redirect(303, "/login/current_password_error") } // Update password const { error } = await supabase.auth.updateUser({ password: newPassword1 }) if (error) return fail(500, { errorMessage: "Unknown error" }) return { success: true } }, } ``` -------------------------------- ### Configure Supabase Local Environment Variables Source: https://github.com/criticalmoments/cmsaasstarter/blob/main/README.md This snippet shows how to configure Supabase environment variables for local development. It defines the public URL, anonymous key, and service role key required for your Supabase project. These should be placed in a .env.local file. ```dotenv PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_SUPABASE_ANON_KEY=your-anon-key PRIVATE_SUPABASE_SERVICE_ROLE=your service_role secret ``` -------------------------------- ### Configure OAuth Providers in Login Configuration Source: https://github.com/criticalmoments/cmsaasstarter/blob/main/README.md This TypeScript code snippet demonstrates how to configure OAuth providers for user authentication. It defines a list of providers to be used, which should be updated based on the chosen authentication methods. An empty array means no OAuth options are enabled. ```typescript const oauthProviders = [ // Add your chosen OAuth providers here, e.g., 'google', 'github' ]; ``` -------------------------------- ### Submit Contact Request with Validation (TypeScript) Source: https://context7.com/criticalmoments/cmsaasstarter/llms.txt A server action to handle contact form submissions, including client-side validation for first name, last name, email format, and message length. Upon successful validation, it inserts the contact request into the `contact_requests` table using Supabase and notifies an administrator via email. It returns validation errors if any fields fail the checks. ```typescript // src/routes/(marketing)/contact_us/+page.server.ts export const actions = { submitContactUs: async ({ request, locals: { supabaseServiceRole } }) => { const formData = await request.formData() const errors: { [fieldName: string]: string } = {} const firstName = formData.get("first_name")?.toString() ?? "" if (firstName.length < 2) errors["first_name"] = "First name is required" const lastName = formData.get("last_name")?.toString() ?? "" if (lastName.length < 2) errors["last_name"] = "Last name is required" const email = formData.get("email")?.toString() ?? "" if (!email.includes("@") || !email.includes(".")) { errors["email"] = "Invalid email" } const message = formData.get("message")?.toString() ?? "" if (message.length > 2000) { errors["message"] = "Message too long (" + message.length + " of 2000)" } if (Object.keys(errors).length > 0) return fail(400, { errors }) // Save to database await supabaseServiceRole.from("contact_requests").insert({ first_name: firstName, last_name: lastName, email, company_name: formData.get("company")?.toString() ?? "", phone: formData.get("phone")?.toString() ?? "", message_body: message, updated_at: new Date(), }) // Notify admin await sendAdminEmail({ subject: "New contact request", body: `From ${firstName} ${lastName}\nEmail: ${email}\nMessage: ${message}`, }) }, } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.