Getting Started with TanStack Start And Convex: Your SaaS Foundation

Learn how to get started with TanStack Start And Convex and start building your first SaaS

Getting Started with TanStack Start And Convex: Your SaaS Foundation

TanStack Start

Part 1 of 1

1
Getting Started with TanStack Start And Convex: Your SaaS Foundation

Welcome to the first article in our series on building a modern SaaS application! We’ll use TanStack Start, a cutting-edge React framework, alongside Convex, Clerk, Radix UI, and Polar.sh to create a full-stack app.

Important Note: TanStack Start is in Alpha/Beta. APIs may change before a stable release. Embrace the innovation, but expect potential updates!

What is TanStack Start?

TanStack Start is a modern React framework from the TanStack team, known for libraries like TanStack Query and Router. It offers:

  • File-based Routing: Similar to Next.js or Remix.
  • Server-Side Rendering (SSR): With client-side hydration.
  • Integrated Data Fetching: Seamless with TanStack Query.
  • Vite Tooling: Fast development and builds.

What is Convex?

Convex is a backend-as-a-service platform that simplifies building scalable, real-time applications. It provides:

  • Realtime Database: Automatically syncs data changes to clients, ideal for dynamic apps. Learn more in the Convex Database Docs.
  • Serverless Functions: Write queries (for reading data) and mutations (for writing data) in TypeScript/JavaScript, hosted by Convex.
  • Type Safety: Shares types between frontend and backend, reducing errors.
  • Dashboard: A web-based Convex Dashboard to manage data, view logs, and debug.
  • Additional Features: File storage, scheduled tasks, and vector search.

Convex integrates seamlessly with TanStack Start via the @convex-dev/react-query package, allowing you to fetch and mutate data using TanStack Query hooks.

Installation (The Easy Way with Convex)

Use the Convex template to scaffold a project with TanStack Start and Convex pre-configured:

npm create convex@latest -- -t tanstack-start my-saas-app
cd my-saas-app

This creates a project in my-saas-app with React, TypeScript, TanStack Start, and Convex client setup. The template includes a convex/ directory for backend logic and pre-configures environment variables.

For manual setup, refer to the TanStack Start Getting Started Guide and integrate Convex in the next article.

Starting the Development Environment

Run the development server to initialize your app and Convex backend:

npm run dev

You’ll see output like this:

> [email protected] dev
> npx convex dev --once && npm-run-all --parallel dev:convex dev:start

? Welcome to Convex! Would you like to login to your account? Start without an account (run Convex locally)
Let's set up your first project.
? Choose a name: my-saas-app
This command, `npx convex dev`, will run your Convex backend locally and update it with the function you write in the `convex/` directory.
Use `npx convex dashboard` to view and interact with your project from a web UI.
Use `npx convex docs` to read the docs and `npx convex help` to see other commands.
? Continue? Yes
✔ Downloaded Convex backend binary
✔ Downloaded Convex dashboard
✔ Started running a deployment locally at http://127.0.0.1:3210 and saved its:
    name as CONVEX_DEPLOYMENT to .env.local
    URL as VITE_CONVEX_URL to .env.local
Run `npx convex login` at any time to create an account and link this deployment.

Write your Convex functions in convex/
Give us feedback at https://convex.dev/community or [email protected]
View the Convex dashboard at http://127.0.0.1:6790/?d=anonymous-my-saas-app

✔ 11:29:40 Convex functions ready! (959.55ms)

> [email protected] dev:start
> vinxi dev

> [email protected] dev:convex
> convex dev

vinxi v0.4.3
vinxi starting dev server

♻️  Generating routes...
✔ Started running a deployment locally at http://127.0.0.1:3210 and saved its name as CONVEX_DEPLOYMENT to .env.local
Run `npx convex login` at any time to create an account and link this deployment.

Write your Convex functions in convex/
Give us feedback at https://convex.dev/community or [email protected]
View the Convex dashboard at http://127.0.0.1:6790/?d=anonymous-my-saas-app

⠋ Preparing Convex functions...
⠙ Preparing Convex functions...

  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose
✔ 11:29:42 Convex functions ready! (839.54ms)
Warning: A notFoundError was encountered on the route with ID "__root__", but a notFoundComponent option was not configured...

What’s Happening?

  • Convex Initialization: npx convex dev --once sets up a local Convex backend, prompting you to name your project (e.g., my-saas-app). It runs at http://127.0.0.1:3210 and saves VITE_CONVEX_URL and CONVEX_DEPLOYMENT to .env.local. These variables connect your frontend to the Convex backend.
  • Parallel Execution: npm-run-all --parallel dev:convex dev:start runs the Convex dev server (npx convex dev) and Vite dev server (vinxi dev) concurrently.
  • Vite Server: Your app is available at http://localhost:3000 (port may vary). Open this URL to see your app.
  • Convex Dashboard: Visit http://127.0.0.1:6790/?d=anonymous-my-saas-app to manage your backend data, view logs, and test functions. Learn more about the dashboard in the Convex Dashboard Docs.
  • Not Found Warning: The notFoundError indicates TanStack Router needs a custom 404 component, which we’ll address below.

Tip: Run npx convex dashboard to open the dashboard directly, or npx convex login to link your local deployment to a Convex account for cloud syncing.

Understanding the Project Structure

Key files and directories:

.
├── app/
│   ├── routes/             # Page routes
│   │   ├── __root.tsx      # Root layout (HTML structure, global providers)
│   │   └── index.tsx       # Homepage component ('/')
│   ├── client.tsx          # Client-side entry (hydrates SSR)
│   ├── router.tsx          # Router setup (integrates Convex/Clerk)
│   ├── routeTree.gen.ts    # Auto-generated route tree
│   └── ssr.tsx             # Server-side rendering entry
├── convex/                 # Backend logic (Convex functions, schema)
│   ├── schema.ts           # Database schema (defines tables)
│   └── ...                 # Backend functions (queries, mutations)
├── public/                 # Static assets
├── .env.local              # Environment variables (e.g., VITE_CONVEX_URL)
├── app.config.ts           # TanStack Start config
├── convex.config.ts        # Convex config
├── package.json
├── tsconfig.json           # TypeScript config
└── vite.config.ts          # Vite config
  • app/routes/__root.tsx: Defines the app’s layout, including <html>, <head>, <body>, and common UI like headers. <Outlet /> renders child routes.
  • app/routes/index.tsx: Renders the / route (homepage).
  • convex/: Contains backend logic. schema.ts defines your database structure (see Convex Schema Docs). Other files will hold queries and mutations.
  • .env.local: Stores sensitive keys like VITE_CONVEX_URL. Never commit this file to Git.

Basic Routing

TanStack Start uses file-based routing via TanStack Router:

  • app/routes/index.tsx: Uses createFileRoute('/') to define the homepage.
  • app/routes/__root.tsx: Uses createRootRouteWithContext to set up the root layout and context (e.g., queryClient for TanStack Query, convexClient for Convex).

Let’s add a header and footer to app/routes/__root.tsx to provide consistent navigation and branding across all pages:

// app/routes/__root.tsx
import { QueryClient } from "@tanstack/react-query";
import { createRootRouteWithContext, Link } from "@tanstack/react-router";
import { Outlet, ScrollRestoration } from "@tanstack/react-router";
import { Meta, Scripts } from "@tanstack/start";
import * as React from "react";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { ConvexReactClient } from "convex/react";

interface MyRouterContext {
  queryClient: QueryClient;
  convexClient: ConvexReactClient;
  convexQueryClient: ConvexQueryClient;
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  head: () => ({
    meta: [
      { charSet: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      { title: "My SaaS App" },
    ],
  }),
  notFoundComponent: () => (
    <div style={{ textAlign: 'center', padding: '2rem' }}>
      <h1>404 - Page Not Found</h1>
      <Link to="/">Go Home</Link>
    </div>
  ),
  component: RootComponent,
});

function RootComponent() {
  return (
    <RootDocument>
      <header
        style={{
          padding: '1rem',
          borderBottom: '1px solid #eee',
          background: '#f8f9fa',
          position: 'sticky',
          top: 0,
          zIndex: 10,
        }}
        role="banner"
      >
        <nav aria-label="Main navigation">
          <Link
            to="/"
            style={{
              marginRight: '1rem',
              fontWeight: 'bold',
              color: '#333',
              textDecoration: 'none',
            }}
            aria-label="Home page"
          >
            My SaaS App
          </Link>
        </nav>
      </header>
      <main style={{ padding: '1rem', minHeight: '80vh' }} role="main">
        <Outlet />
      </main>
      <footer
        style={{
          padding: '1rem',
          borderTop: '1px solid #eee',
          textAlign: 'center',
          background: '#f8f9fa',
        }}
        role="contentinfo"
      >
        <p>© {new Date().getFullYear()} My SaaS App. All rights reserved.</p>
      </footer>
    </RootDocument>
  );
}

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <Meta />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Detailed Explanation of Header and Footer Code:

  • RootComponent: This function defines the core layout of your application, rendered for every route. It wraps the <header>, <main>, and <footer> in a RootDocument component, which provides the HTML structure (<html>, <head>, <body>).

    • Purpose: Ensures consistent UI elements (like navigation and branding) appear on all pages.
    • Structure: Uses semantic HTML elements (<header>, <main>, <footer>) for accessibility and SEO.
  • Header:

    • Element: <header role="banner"> marks the header as the primary banner of the page, improving accessibility for screen readers.
    • Styling:
      • padding: '1rem': Adds spacing for a clean look.
      • borderBottom: '1px solid #eee': Adds a subtle divider.
      • background: '#f8f9fa': Uses a light gray background for visual distinction.
      • position: 'sticky', top: 0, zIndex: 10: Keeps the header fixed at the top during scrolling, ensuring navigation is always accessible.
    • Navigation:
      • <nav aria-label="Main navigation">: Labels the navigation for accessibility.
      • <Link to="/" ...>My SaaS App</Link>: Uses TanStack Router’s Link component for client-side navigation to the homepage (/).
        • Styling: fontWeight: 'bold', color: '#333', textDecoration: 'none' makes the link prominent and removes the default underline.
        • Accessibility: aria-label="Home page" provides context for screen readers.
    • Purpose: The header provides a consistent navigation bar, starting with a single link to the homepage. Later articles will add more links (e.g., to chat or pricing pages).
  • Main:

    • Element: <main role="main"> designates the primary content area, aiding accessibility.
    • Styling: padding: '1rem', minHeight: '80vh' ensures content is spaced and the main area takes up most of the viewport height, preventing the footer from appearing too high on short pages.
    • Outlet: <Outlet /> is a TanStack Router component that renders the content of the current route (e.g., index.tsx for /).
  • Footer:

    • Element: <footer role="contentinfo"> marks the footer as supplementary information, enhancing accessibility.
    • Styling:
      • padding: '1rem': Adds spacing.
      • borderTop: '1px solid #eee': Adds a divider.
      • textAlign: 'center': Centers the text.
      • background: '#f8f9fa': Matches the header’s background for consistency.
    • Content: Displays a dynamic copyright notice using the current year (new Date().getFullYear()).
    • Purpose: Provides a professional touch and space for additional links or information in the future.
  • RootDocument:

    • Purpose: Defines the HTML structure required for SSR and client-side rendering.
    • Components:
      • <Meta />: Injects metadata (e.g., <title>, <meta> tags) defined in the head function.
      • <ScrollRestoration />: Ensures scroll position is preserved during navigation.
      • <Scripts />: Includes JavaScript bundles for client-side interactivity.
    • Accessibility: Sets lang="en" on <html> for language clarity.
  • NotFoundComponent:

    • Addresses the notFoundError warning by providing a custom 404 page.
    • Includes a simple message and a link back to the homepage.
  • Accessibility Considerations:

    • Semantic elements (header, main, footer) and ARIA roles (banner, main, contentinfo) improve screen reader compatibility.
    • aria-label on navigation elements enhances usability for assistive technologies.
    • Styling ensures sufficient contrast (e.g., #333 text on #f8f9fa background).
  • Extensibility: The header’s <nav> can later include additional <Link> components for routes like /chat or /pricing, as shown in later articles.

Troubleshooting Common Issues

  • Port Conflicts: If localhost:3000 is taken, Vite uses another port (check terminal output).
  • Convex Setup Fails: Ensure internet connectivity. Run npx convex dev --once manually if needed. Check the Convex Troubleshooting Docs.
  • Not Found Warning: The notFoundComponent above resolves this. Alternatively, set defaultNotFoundComponent in app/router.tsx.
  • Environment Variables: Verify .env.local contains VITE_CONVEX_URL and CONVEX_DEPLOYMENT. See Convex Environment Variables.
  • Convex Dashboard Access: If the dashboard URL fails, run npx convex dashboard or check firewall settings.

Next, we’ll integrate Convex for the backend and Clerk for authentication, building on the foundation laid here!

Related Posts