Skip to content

tacosjs/tanstack-starter-e2e-encryption

Repository files navigation

Tanstack Start - E2E Encrypted SaaS Starter

! Not yet ready. Currently in development phase.

A production-ready (soon) SaaS dashboard starter built with TanStack Start, Convex, and Clerk — with end-to-end encryption baked in from day one.

Use this starter as the foundation for any SaaS that handles sensitive user data: note-taking apps, document editors, personal finance tools, health trackers, or anything where users deserve to know their data is private by design.

The Problem With Most SaaS

Most SaaS products store user data in plaintext. This means the company (and anyone who gains access to their infrastructure) can read your users' data at any given time.

This starter takes a different approach. Sensitive data is encrypted on the client before it ever leaves the browser. The server only stores ciphertext and it has no access to decryption keys or plaintext, ever.

What This Starter Provides

A full-stack SaaS dashboard template where you can build features on top of a privacy-first foundation:

  • Authenticated dashboard with per-user encrypted document storage
  • Client-side encryption — data is encrypted before upload, decrypted after download
  • Zero-knowledge key management — keys are derived from the user's password, never sent to the server
  • Real-time sync via Convex, without ever exposing plaintext to the backend
  • Drop-in auth via Clerk with seamless key provisioning on sign-up/sign-in

Tech Stack

  • Frontend: TanStack Start, TanStack Router, TanStack Query
  • Backend: Convex (real-time database)
  • Auth: Clerk
  • Encryption: OpenPGP.js (ECC P-256), @scure/bip39 (passphrase generation)
  • UI: Tailwind CSS, shadcn, Radix UI
  • State: Jotai

How It Works — Zero-Knowledge Architecture

This is the foundation of a zero-knowledge architecture: the service provider genuinely cannot read what users store.

sequenceDiagram
    participant U as User
    participant C as Client
    participant Cl as Clerk
    participant Cx as Convex

    note over U,Cx: Sign-Up
    U->>C: email + password
    C->>Cl: signUp.create()
    Cl-->>U: verification email
    U->>C: verification code
    C->>Cl: attemptEmailAddressVerification()
    Cl-->>C: createdSessionId
    Note over C: createPassphrase() → 12-word BIP39 mnemonic
    Note over C: encryptPassphrase(passphrase, password) → PGP symmetric
    Note over C: generateKeyPair(email, passphrase) → ECC nistP256
    C->>Cx: createUserKeys({ encryptedPassphrase, encryptedPrivateKey, publicKey })
    Note over C: passphrase stored in Jotai atom (memory only, never persisted)

    note over U,Cx: Sign-In
    U->>C: email + password
    C->>Cl: signIn.create()
    Cl-->>C: session
    C->>Cx: getUserKeys()
    Cx-->>C: { encryptedPassphrase, encryptedPrivateKey, publicKey }
    Note over C: decryptPassphrase(encryptedPassphrase, password)
    Note over C: passphrase restored to Jotai atom (memory only)

    note over U,Cx: Encrypt Data
    C->>Cx: getUserKeys() → publicKey
    Note over C: encryptWithPublicKey(plaintext, publicKey) → Armored encrypted message
    C->>Cx: store ciphertext

    note over U,Cx: Decrypt Data
    C->>Cx: getUserKeys() → encryptedPrivateKey (reactive, already in memory)
    C->>Cx: fetch ciphertext (application data)
    Cx-->>C: encryptedPrivateKey + ciphertext
    Note over C: passphrase from Jotai atom (memory only)
    Note over C: decryptWithPrivateKey(encryptedData, encryptedPrivateKey, passphrase) → Decrypted plaintext
    C-->>U: plaintext (never sent to server)
Loading
  • Key generation: PGP key pairs (ECC nistP256) generated client-side in src/lib/crypto.ts during signup
  • Passphrase: 12-word BIP39 mnemonic (128-bit entropy), encrypted with the user's Clerk password before storage
  • Storage: Convex stores only encrypted blobs—encryptedPrivateKey, publicKey, encryptedPassphrase
  • Usage: useEncryption hook (src/hooks/useEncryption.ts) encrypts/decrypts data client-side; Convex cannot decrypt

Prerequisites

Getting Started

npm install # Install dependencies packages
cp .env.example .env.local # Edit .env.local with your keys
npm run start # Starts Convex dev + Vite (port 3666)

Required environment variables (from .env.example):

Variable Description
VITE_CLERK_PUBLISHABLE_KEY From Clerk Dashboard
CLERK_JWT_ISSUER_DOMAIN For Convex JWT verification
VITE_CONVEX_URL, CONVEX_DEPLOYMENT Run npx convex init or set manually

Scripts

Script Description
npm run start Convex dev + Vite dev server
npm run dev Vite only (port 3666)
npm run build Production build
npm run test Vitest
npm run lint ESLint
npm run format Prettier

Project Structure

  • src/routes/ — TanStack Router file-based routing
  • src/hooks/useSignUp, useSignIn, useEncryption
  • src/lib/crypto.ts — PGP key generation, encrypt/decrypt
  • convex/ — Convex backend (userKeys, auth config)
  • src/integrations/ — Clerk, Convex providers

Additional Features

Paraglide i18n: Messages in project.inlang/messages; URLs localized via Paraglide Vite plugin and router rewrite hooks.

shadcn: Add components with pnpm dlx shadcn@latest add button

T3 Env: Type-safe environment variables. Add variables in src/env.ts, use via import { env } from '@/env'.

Learn More

About

A TanStack Start starter with Convex and Clerk that uses client-side end-to-end encryption to keep sensitive data encrypted

Resources

Security policy

Stars

Watchers

Forks

Contributors