Skip to content
Framework: Vite + React + TypeScriptDifficulty: intermediateFeatured

SaaS starter

Marketing pages, sign-in and sign-up screens, and an authenticated app area with a mocked session you replace with real auth.

A multi-page single-page app built on a tiny hash router (no extra dependency): landing, pricing, sign in, sign up, an app dashboard and a settings page. Authentication is deliberately mocked in src/auth.tsx and labelled as such in the UI; the session lives in localStorage so you can click through every flow before wiring up a real provider.

  • react
  • saas
  • auth
  • multi-page
  • tailwind
  • typescript

This preview is the template's actual file tree and source code — exactly what is copied into a new project. Codexa does not publish screenshots or mock-ups of templates; run the project to see it rendered.

Files18 files

src/pages/Auth.tsx93 lines
import { useState, type FormEvent } from "react";
import { useAuth } from "../auth";
import { Link, navigate } from "../router";

function MockNotice() {
  return (
    <p className="rounded-md border border-amber-300 bg-amber-50 p-3 text-xs text-amber-900">
      <strong>Mock authentication.</strong> Any email is accepted and nothing is verified. Replace <code>src/auth.tsx</code> with real auth before shipping.
    </p>
  );
}

export function SignIn({ redirectTo }: { redirectTo: string }) {
  const { signIn } = useAuth();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);

  function submit(e: FormEvent) {
    e.preventDefault();
    if (!email.includes("@")) return setError("Enter a valid email address.");
    if (password.length < 8) return setError("Password must be at least 8 characters.");
    signIn(email);
    navigate(redirectTo);
  }

  return (
    <form onSubmit={submit} className="mx-auto max-w-sm space-y-4 rounded-xl border border-slate-200 bg-white p-6">
      <h1 className="text-xl font-semibold">Sign in</h1>
      <MockNotice />
      <label className="block text-sm">
        <span className="font-medium">Email</span>
        <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" required className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
      </label>
      <label className="block text-sm">
        <span className="font-medium">Password</span>
        <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" required className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
      </label>
      {error ? (
        <p role="alert" className="text-sm text-red-600">
          {error}
        </p>
      ) : null}
      <button type="submit" className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
        Sign in
      </button>
      <p className="text-center text-sm text-slate-600">
        No account? <Link to="/signup" className="text-blue-600">Sign up</Link>
      </p>
    </form>
  );
}

export function SignUp() {
  const { signUp } = useAuth();
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [error, setError] = useState<string | null>(null);

  function submit(e: FormEvent) {
    e.preventDefault();
    if (name.trim().length < 2) return setError("Enter your name.");
    if (!email.includes("@")) return setError("Enter a valid email address.");
    signUp(name.trim(), email);
    navigate("/app");
  }

  return (
    <form onSubmit={submit} className="mx-auto max-w-sm space-y-4 rounded-xl border border-slate-200 bg-white p-6">
      <h1 className="text-xl font-semibold">Create your account</h1>
      <MockNotice />
      <label className="block text-sm">
        <span className="font-medium">Name</span>
        <input value={name} onChange={(e) => setName(e.target.value)} required className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
      </label>
      <label className="block text-sm">
        <span className="font-medium">Email</span>
        <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" required className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
      </label>
      {error ? (
        <p role="alert" className="text-sm text-red-600">
          {error}
        </p>
      ) : null}
      <button type="submit" className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
        Create account
      </button>
      <p className="text-center text-sm text-slate-600">
        Already registered? <Link to="/signin" className="text-blue-600">Sign in</Link>
      </p>
    </form>
  );
}

Dependencies

Documented from the template's package.json. The runtime has a prebuilt dependency set for Vite + React + TypeScript, so installs are fast.

PackageVersion
react^19.1.0
react-dom^19.1.0
vite^6.3.0
@vitejs/plugin-react^4.4.0
tailwindcss^4.1.0
@tailwindcss/vite^4.1.0
typescript^5.8.0

How it runs

Framework
Vite + React + TypeScript
Dev command
npm run dev
Build command
npm run build
Output directory
dist
Port
5173
Opens at
/
Difficulty
intermediate

Projects run in an isolated sandbox with a live preview. Read the projects documentation for details on settings, snapshots and deployment.

Need the raw data? GET /api/v1/templates/vite-react-saas returns this template with its files.

Build on SaaS starter

Create a free account, start from this template, and ask the AI agent to turn it into your app.