diff --git a/.explorer/better-auth-extended b/.explorer/better-auth-extended
new file mode 160000
index 0000000..6e299bc
--- /dev/null
+++ b/.explorer/better-auth-extended
@@ -0,0 +1 @@
+Subproject commit 6e299bc9796513412aa1302735a8057afa2b0324
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000..9309e20
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,47 @@
+name: Verify templates
+on:
+ pull_request:
+ push:
+ branches: [dev]
+ workflow_dispatch:
+permissions:
+ contents: read
+jobs:
+ shared:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: "24"
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: "1.3.14"
+ - run: bun install --frozen-lockfile
+ - run: bun run lint
+ - run: bun run check-types
+ - run: bun run test
+ - run: bun audit
+ presets:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ mode: [personal, organization]
+ frontend: [react, tanstack]
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: "24"
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: "1.3.14"
+ - run: bun run setup --mode ${{ matrix.mode }} --frontend ${{ matrix.frontend }}
+ - run: bun install --frozen-lockfile
+ - run: bun run check-types
+ - run: bun run test
+ - run: bun run build
+ - if: matrix.frontend == 'tanstack'
+ run: node scripts/verify-tanstack-runtime.mjs
+ - run: bun audit
diff --git a/.gitignore b/.gitignore
index 61ed6c9..77bea20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@ build
# Environment
.env
+.env.*
+!.env.example
.env.local
.env.*.local
.env.prod
@@ -43,6 +45,8 @@ ehthumbs.db
# Turbo
.turbo
+.tanstack
+.nitro
# Logs
*.log
@@ -53,3 +57,5 @@ yarn-error.log*
*.tsbuildinfo
.sisyphus
+.explorer/better-auth-ui
+opencode.json
diff --git a/README.md b/README.md
index ce47d56..e017c2e 100644
--- a/README.md
+++ b/README.md
@@ -1,197 +1,90 @@
# FlowStack
-**FlowStack** is a production-grade SaaS foundation focused on **clean architecture, clear boundaries, and long-term maintainability**.
+A shared SaaS starter with two account models and two frontend choices.
+Maintain the foundation once, then configure it when starting a project.
-It is not a framework.
-It is not a boilerplate with magic.
+## Quick start
-FlowStack is a **base repository** designed to help you build scalable products without losing control of your codebase.
+Install Node.js 24 LTS and Bun 1.3.14, then:
----
-
-## Why FlowStack?
-
-Most starters focus on **tech stack choices**.
-
-FlowStack focuses on **flow**:
-
-- how identity flows
-- how permissions flow
-- how responsibility flows
-- how code grows without becoming messy
-
-The goal is simple:
-
-> **Make the architecture obvious, boring, and easy to evolve.**
-
----
-
-## Core Principles
-
-### 1. One Responsibility per File
-
-- One action per file
-- One API per file
-- One schema per file
-
-No large "god files".
-
----
-
-### 2. One Responsibility per Folder
-
-Folders represent **domains**, not features.
-
-Examples:
-
-- `auth` → identity (who are you?)
-- `access` → permissions (what can you do?)
-- `impersonation` → temporary identity
-- `platform` → operator / super-admin logic
-
-If a folder exists, the feature exists.
-No runtime feature flags.
-
----
-
-### 3. No Runtime Branching for Product Shape
-
-There are **no** `if (config.xxx)` checks inside business logic.
-
-All variability is resolved at **generation time**:
-
-- modules are included or excluded
-- unused folders are removed
-- runtime code stays clean and predictable
-
----
-
-### 4. Apps Compose, Packages Own Logic
-
-- `apps/` contain routing and wiring
-- `packages/` contain real logic
-
-Apps never own business rules.
-
----
-
-### 5. Boring Code > Clever Code
-
-FlowStack prefers:
-
-- explicit files
-- explicit imports
-- explicit boundaries
-
-Over abstraction is avoided on purpose.
-
----
-
-## Core Stack (Defaults, Not Lock-in)
-
-FlowStack is **stack-aware**, but not stack-locked.
-
-See **[docs/stack.md](./docs/stack.md)** for the default technologies and design philosophy.
-
-These are tools FlowStack is built and tested with. You can replace parts of the stack if you know what you're doing.
-
----
-
-## Authentication
-
-FlowStack uses [Better Auth](https://www.better-auth.com/) for identity management with support for:
-
-- Email/password authentication
-- Magic link authentication
-- OAuth providers (Google, GitHub, etc.)
-- Session management
-- Account settings and security
-
-For detailed setup, configuration, and troubleshooting guides, see **[docs/auth/README.md](./docs/auth/README.md)**.
-
----
-
-## High-Level Structure
-
-```
-apps/
- web/ # Customer frontend
- server/ # Customer API
- super-admin/ # Operator panel (optional)
-
-packages/
- auth/ # Identity
- access/ # Authorization (RBAC)
- impersonation/ # Temporary identity
- platform/ # Operator-level control
- db/ # Database schema & migrations
- env/ # Typed environment
- email/ # Email providers & templates
- storage/ # File storage
- workflows/ # Background jobs
+```sh
+git clone --branch dev https://github.com/jacksonkasi1/FlowStack.git my-app
+cd my-app
+bun run setup
```
-Each package is **independently understandable**.
+Setup asks only:
----
+1. **Personal or Organization** — individual accounts, or team workspaces with
+ onboarding, invitations and membership.
+2. **React Router or TanStack Start** — both use React; the backend is Hono on Node.js.
-## Configuration
+The auth, database, email and storage packages are included automatically. Setup
+installs dependencies and creates missing environment files from examples. Fill
+in your credentials, run migrations, then start your chosen frontend and API:
-All feature decisions live in one place:
-
-```ts
-flow.config.ts;
+```sh
+bun run --cwd packages/db db:migrate
+bun run dev
```
-This file answers **what exists**, not **how it works**.
-
-Example:
-
-- auth mode
-- super-admin enabled or not
-- impersonation enabled or not
-- deployment targets
-
-Runtime code assumes the decision is already made.
-
----
-
-## What This Repo Is (and Isn't)
-
-✅ A clean, extensible foundation
-✅ A reference architecture
-✅ A long-term base for real products
-
-❌ Not a "plug and play" SaaS
-❌ Not opinionated about UI design
-❌ Not a low-code framework
+See [the setup guide](docs/getting-started/template-setup.md) for environment
+configuration, optional cleanup and deployment details.
-You are expected to **build on top of it**.
+## Separate starter copies
----
-
-## Current Status
-
-FlowStack is an **active base repository**.
-
-Features will be added incrementally:
-
-- more auth flows
-- more workflow primitives
-- more deployment helpers
-
-Breaking changes may happen early while the foundation is being refined.
+```sh
+bun run setup --mode personal --frontend react --output ../my-app
+bun run setup --mode organization --frontend tanstack --output ../team-app
+```
----
+Generated copies exclude Git history, environment files, dependencies and build
+output. The destination must be empty. Omit `--output` to configure the current
+clone, or add `--no-install` to prepare files only.
+
+Setup retains source for both frontends and selects one workspace. You may remove
+the unselected frontend manually afterward. Personal mode disables organization
+and onboarding server plugins and UI. Both presets retain a shared database schema
+and migration history. Configuration lives in
+`packages/config/src/config/preset.ts`, with auth policy in `auth-mode.ts`.
+
+## Structure
+
+- `apps/web`: React + React Router frontend.
+- `apps/tanstack`: TanStack Start frontend.
+- `apps/server`: Hono API with a Node.js entry point.
+- `packages/auth`, `auth-ui`, `onboarding`: authentication, guards and onboarding.
+- `packages/config`, `db`, `email`, `email-templates`, `storage`, `logs`: shared foundation.
+- `scripts/setup.mjs`: interactive and scripted setup.
+- `tests`: setup, validation, guard and database-backed auth tests.
+
+## Verification and maintenance
+
+```sh
+bun run lint
+bun run check-types
+bun run test
+bun run build
+bun audit
+bun run verify:presets
+```
-## Philosophy
+The auth integration suite uses embedded PostgreSQL and a stubbed email transport;
+it does not contact your database or send real email. CI checks both account models
+with both frontends. See [upgrade notes](docs/advanced/unified-template-upgrade.md)
+for dependency compatibility, migrations and the new storage ownership rules.
-> Scale is not about features.
-> Scale is about clarity.
+Development is on `dev`; releases are reviewed through PRs to `main`. Historical
+backups are preserved. Existing customized projects do not update automatically:
+use their `.flowstack.json` source commit and release notes to apply fixes.
-FlowStack exists to keep that clarity intact as products grow.
+## Documentation
----
+- [Setup and presets](docs/getting-started/template-setup.md)
+- [Authentication](docs/auth/README.md)
+- [Architecture](docs/concepts/architecture.md)
+- [Upgrade notes](docs/advanced/unified-template-upgrade.md)
+- [Documentation index](docs/README.md)
## License
diff --git a/apps/server/.env.example b/apps/server/.env.example
index bcb0492..56c78d1 100644
--- a/apps/server/.env.example
+++ b/apps/server/.env.example
@@ -36,3 +36,9 @@ R2_PUBLIC_URL=https://your-public-domain.com
# Skip validation during build (optional)
# SKIP_ENV_VALIDATION=true
+
+# Invitation flow and multi-organization policy (keep server/frontend values aligned)
+VITE_INVITE_DIRECT_SIGNUP=true
+VITE_INVITE_LOCK_EMAIL=true
+VITE_INVITE_SKIP_ORGANIZATION_ONBOARDING=true
+VITE_ALLOW_MULTIPLE_ORGANIZATIONS=false
diff --git a/apps/server/.gitignore b/apps/server/.gitignore
new file mode 100644
index 0000000..e985853
--- /dev/null
+++ b/apps/server/.gitignore
@@ -0,0 +1 @@
+.vercel
diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile
index 3726ba3..1eb60e8 100644
--- a/apps/server/Dockerfile
+++ b/apps/server/Dockerfile
@@ -1,4 +1,4 @@
-FROM oven/bun:1
+FROM oven/bun:1.3.14 AS build
WORKDIR /app
# Copy root workspace config
@@ -6,14 +6,19 @@ COPY package.json bun.lock turbo.json ./
# Copy packages and app source
COPY packages ./packages
-COPY apps/server ./apps/server
+COPY apps ./apps
# Install dependencies
-RUN bun install
+RUN bun install --frozen-lockfile
# Build the server
WORKDIR /app/apps/server
RUN bun run build
-# Start the server
-CMD ["bun", "run", "dist/index.js"]
+FROM node:24-alpine AS runtime
+WORKDIR /app
+ENV NODE_ENV=production
+COPY --from=build /app/apps/server/dist ./dist
+USER node
+EXPOSE 8080
+CMD ["node", "dist/index.js"]
diff --git a/apps/server/package.json b/apps/server/package.json
index 9aee28b..9aca335 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -3,30 +3,35 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "bun run --hot src/index.ts",
- "build": "tsc",
+ "dev": "tsx watch --env-file=.env src/index.ts",
+ "build": "bun build src/index.ts --target=node --outdir=dist",
"lint": "eslint src",
"format": "prettier --write .",
"check": "prettier --write . && eslint --fix",
"check-types": "tsc --noEmit",
"deploy:prod": "./.deploy/scripts/deploy.sh prod",
"deploy:beta": "./.deploy/scripts/deploy.sh beta",
- "deploy:sandbox": "./.deploy/scripts/deploy.sh sandbox"
+ "deploy:sandbox": "./.deploy/scripts/deploy.sh sandbox",
+ "start": "node --env-file=.env dist/index.js"
},
"dependencies": {
"@repo/auth": "workspace:*",
"@repo/db": "workspace:*",
"@repo/logs": "workspace:*",
- "@repo/shared": "workspace:*",
+ "@repo/config": "workspace:*",
"@repo/storage": "workspace:*",
- "@t3-oss/env-core": "^0.13.10",
- "hono": "^4.11.4",
- "zod": "^4.3.5"
+ "@t3-oss/env-core": "^0.13.11",
+ "hono": "^4.13.7",
+ "zod": "^4.5.4",
+ "@hono/node-server": "^2.1.1",
+ "@hono/zod-validator": "^0.9.1",
+ "drizzle-orm": "^0.45.2"
},
"devDependencies": {
- "@repo/typescript-config": "*",
- "@types/node": "^22",
- "prettier": "^3.8.0",
- "typescript": "^5.7.2"
+ "@repo/typescript-config": "workspace:*",
+ "@types/node": "^24.0.0",
+ "prettier": "^3.9.6",
+ "typescript": "~6.0.3",
+ "tsx": "^4.23.13"
}
}
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index b597955..f1e3652 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -1,3 +1,4 @@
+import { serve } from "@hono/node-server";
// ** import core packages
import { Hono } from "hono";
import { cors } from "hono/cors";
@@ -55,9 +56,8 @@ app.onError((err, c) => {
const PORT = parseInt(env.PORT || "8080", 10);
-export default {
- port: PORT,
- fetch: app.fetch,
-};
+serve({ port: PORT, fetch: app.fetch });
+
+export default app;
logger.info(`Server starting on http://localhost:${PORT}`);
diff --git a/apps/server/src/middleware/auth.ts b/apps/server/src/middleware/auth.ts
index bc06958..2a1ec43 100644
--- a/apps/server/src/middleware/auth.ts
+++ b/apps/server/src/middleware/auth.ts
@@ -1,3 +1,7 @@
+import { db, member } from "@repo/db";
+import { and, eq } from "drizzle-orm";
+import { isOrganizationMode } from "@repo/config";
+import { userPrefix, organizationPrefix } from "../routes/storage/validation";
// ** import types
import type { Context, Next } from "hono";
@@ -21,11 +25,31 @@ export async function authMiddleware(
const auth = getAuthInstance();
const session = await auth.api.getSession({
headers: c.req.raw.headers,
+ query: { disableCookieCache: true },
});
c.set("session", session?.session || null);
c.set("user", session?.user || null);
+ if (session?.user) {
+ const prefixes = [userPrefix(session.user.id)];
+ const orgId = (session.session as { activeOrganizationId?: string })
+ .activeOrganizationId;
+ if (isOrganizationMode() && orgId) {
+ const membership = await db
+ .select({ id: member.id })
+ .from(member)
+ .where(
+ and(
+ eq(member.userId, session.user.id),
+ eq(member.organizationId, orgId),
+ ),
+ )
+ .limit(1);
+ if (membership.length) prefixes.push(organizationPrefix(orgId));
+ }
+ c.set("storagePrefixes", prefixes);
+ }
return next();
}
diff --git a/apps/server/src/routes/storage/delete.ts b/apps/server/src/routes/storage/delete.ts
index 4865262..dd94027 100644
--- a/apps/server/src/routes/storage/delete.ts
+++ b/apps/server/src/routes/storage/delete.ts
@@ -1,13 +1,16 @@
+import { fileQuery, ownsPath } from "./validation";
// ** import lib
import { Hono } from "hono";
// ** import utils
import { r2 } from "@repo/storage";
-const route = new Hono();
+const route = new Hono<{ Variables: { storagePrefixes: string[] } }>();
-route.delete("/delete", async (c) => {
- const filePath = c.req.query("filePath");
+route.delete("/delete", fileQuery, async (c) => {
+ const { filePath } = c.req.valid("query");
+ if (!ownsPath(filePath, c.get("storagePrefixes") || []))
+ return c.json({ error: "Forbidden" }, 403);
if (!filePath) {
return c.json({ error: "filePath is required" }, 400);
diff --git a/apps/server/src/routes/storage/download.ts b/apps/server/src/routes/storage/download.ts
index 951b882..33108a6 100644
--- a/apps/server/src/routes/storage/download.ts
+++ b/apps/server/src/routes/storage/download.ts
@@ -1,13 +1,16 @@
+import { fileQuery, ownsPath } from "./validation";
// ** import core packages
import { Hono } from "hono";
// ** import utils
import { r2 } from "@repo/storage";
-const route = new Hono();
+const route = new Hono<{ Variables: { storagePrefixes: string[] } }>();
-route.get("/download-url", async (c) => {
- const filePath = c.req.query("filePath");
+route.get("/download-url", fileQuery, async (c) => {
+ const { filePath } = c.req.valid("query");
+ if (!ownsPath(filePath, c.get("storagePrefixes") || []))
+ return c.json({ error: "Forbidden" }, 403);
if (!filePath) {
return c.json({ error: "filePath is required" }, 400);
diff --git a/apps/server/src/routes/storage/exists.ts b/apps/server/src/routes/storage/exists.ts
index 4308f06..35cc634 100644
--- a/apps/server/src/routes/storage/exists.ts
+++ b/apps/server/src/routes/storage/exists.ts
@@ -1,13 +1,16 @@
+import { fileQuery, ownsPath } from "./validation";
// ** import core packages
import { Hono } from "hono";
// ** import utils
import { r2 } from "@repo/storage";
-const route = new Hono();
+const route = new Hono<{ Variables: { storagePrefixes: string[] } }>();
-route.get("/exists", async (c) => {
- const filePath = c.req.query("filePath");
+route.get("/exists", fileQuery, async (c) => {
+ const { filePath } = c.req.valid("query");
+ if (!ownsPath(filePath, c.get("storagePrefixes") || []))
+ return c.json({ error: "Forbidden" }, 403);
if (!filePath) {
return c.json({ error: "filePath is required" }, 400);
diff --git a/apps/server/src/routes/storage/list.ts b/apps/server/src/routes/storage/list.ts
index bf13353..e36f006 100644
--- a/apps/server/src/routes/storage/list.ts
+++ b/apps/server/src/routes/storage/list.ts
@@ -1,20 +1,24 @@
+import { listQuery, ownsPath } from "./validation";
// ** import core packages
import { Hono } from "hono";
// ** import utils
import { r2 } from "@repo/storage";
-const route = new Hono();
+const route = new Hono<{ Variables: { storagePrefixes: string[] } }>();
-route.get("/get-all", async (c) => {
- const prefix = c.req.query("prefix");
- const maxKeys = c.req.query("maxKeys");
- const continuationToken = c.req.query("continuationToken");
+route.get("/get-all", listQuery, async (c) => {
+ const query = c.req.valid("query");
+ const prefixes: string[] = c.get("storagePrefixes") || [];
+ const prefix = query.prefix || prefixes[0];
+ if (!prefix || !ownsPath(prefix, prefixes))
+ return c.json({ error: "Forbidden" }, 403);
+ const { maxKeys, continuationToken } = query;
try {
const result = await r2.listFiles({
prefix: prefix || undefined,
- maxKeys: maxKeys ? parseInt(maxKeys, 10) : undefined,
+ maxKeys,
continuationToken: continuationToken || undefined,
});
diff --git a/apps/server/src/routes/storage/upload.ts b/apps/server/src/routes/storage/upload.ts
index 8905b5f..12f98dc 100644
--- a/apps/server/src/routes/storage/upload.ts
+++ b/apps/server/src/routes/storage/upload.ts
@@ -1,3 +1,4 @@
+import { uploadQuery, organizationPrefix } from "./validation";
// ** import lib
import { Hono } from "hono";
@@ -7,12 +8,16 @@ import { r2 } from "@repo/storage";
// ** import config
import { env } from "@/config/env";
-const route = new Hono();
+const route = new Hono<{ Variables: { storagePrefixes: string[] } }>();
-route.get("/upload-url", async (c) => {
- const fileName = c.req.query("fileName");
- const contentType = c.req.query("contentType");
- const organizationId = c.req.query("organizationId");
+route.get("/upload-url", uploadQuery, async (c) => {
+ const { fileName, contentType, organizationId } = c.req.valid("query");
+ const prefixes: string[] = c.get("storagePrefixes") || [];
+ const prefix = organizationId
+ ? organizationPrefix(organizationId)
+ : prefixes[0];
+ if (!prefix || !prefixes.includes(prefix))
+ return c.json({ error: "Forbidden" }, 403);
if (!fileName) {
return c.json({ error: "fileName is required" }, 400);
@@ -26,7 +31,7 @@ route.get("/upload-url", async (c) => {
try {
const result = await r2.getSignedUploadUrl(fileName, {
contentType: contentType || undefined,
- organizationId: organizationId || undefined,
+ prefix,
});
// Construct publicUrl from R2_PUBLIC_URL + filePath
diff --git a/apps/server/src/routes/storage/validation.ts b/apps/server/src/routes/storage/validation.ts
new file mode 100644
index 0000000..76d1fbe
--- /dev/null
+++ b/apps/server/src/routes/storage/validation.ts
@@ -0,0 +1,49 @@
+/* eslint-disable no-control-regex -- Reject control characters in storage keys. */
+import { z } from "zod";
+import { zValidator } from "@hono/zod-validator";
+
+const safePath = z
+ .string()
+ .min(1)
+ .max(2048)
+ .refine(
+ (value) => !/\.\.|[\\\x00-\x1f]|^\//.test(value),
+ "Invalid file path",
+ );
+export const fileQuery = zValidator("query", z.object({ filePath: safePath }));
+export const uploadQuery = zValidator(
+ "query",
+ z.object({
+ fileName: z
+ .string()
+ .min(1)
+ .max(255)
+ .refine(
+ (value) => !/\.\.|[/\\\x00-\x1f]/.test(value),
+ "Invalid file name",
+ ),
+ contentType: z.string().min(1).max(255).optional(),
+ organizationId: z.string().min(1).max(128).optional(),
+ }),
+);
+export const listQuery = zValidator(
+ "query",
+ z.object({
+ prefix: safePath.optional(),
+ maxKeys: z.coerce.number().int().min(1).max(1000).optional(),
+ continuationToken: z.string().min(1).max(4096).optional(),
+ }),
+);
+
+export function userPrefix(userId: string) {
+ return `uploads/users/${encodeURIComponent(userId)}/`;
+}
+export function organizationPrefix(organizationId: string) {
+ return `uploads/organizations/${encodeURIComponent(organizationId)}/`;
+}
+export function ownsPath(path: string, prefixes: readonly string[]) {
+ return (
+ safePath.safeParse(path).success &&
+ prefixes.some((prefix) => path.startsWith(prefix))
+ );
+}
diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json
index 69f4e13..b1809c6 100644
--- a/apps/server/tsconfig.json
+++ b/apps/server/tsconfig.json
@@ -3,9 +3,8 @@
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
- "baseUrl": ".",
"paths": {
- "@/*": ["src/*"]
+ "@/*": ["./src/*"]
},
"jsx": "react-jsx",
"types": ["node"]
diff --git a/apps/tanstack/.env.example b/apps/tanstack/.env.example
index bf0fb27..fcf2e3c 100644
--- a/apps/tanstack/.env.example
+++ b/apps/tanstack/.env.example
@@ -6,3 +6,9 @@
VITE_API_BASE_URL=http://localhost:8080
# Add any client-side variables here (must be prefixed with VITE_)
+
+# Invitation flow and multi-organization policy (keep server/frontend values aligned)
+VITE_INVITE_DIRECT_SIGNUP=true
+VITE_INVITE_LOCK_EMAIL=true
+VITE_INVITE_SKIP_ORGANIZATION_ONBOARDING=true
+VITE_ALLOW_MULTIPLE_ORGANIZATIONS=false
diff --git a/apps/tanstack/eslint.config.js b/apps/tanstack/eslint.config.js
index 676b32a..cdc8474 100644
--- a/apps/tanstack/eslint.config.js
+++ b/apps/tanstack/eslint.config.js
@@ -1,5 +1 @@
-// @ts-check
-
-import { tanstackConfig } from '@tanstack/eslint-config'
-
-export default [...tanstackConfig]
+export { default } from '../../eslint.config.mjs'
diff --git a/apps/tanstack/package.json b/apps/tanstack/package.json
index 83d9f37..a59a9ac 100644
--- a/apps/tanstack/package.json
+++ b/apps/tanstack/package.json
@@ -9,51 +9,55 @@
"test": "vitest run",
"lint": "eslint",
"format": "prettier",
- "check": "prettier --write . && eslint --fix"
+ "check": "prettier --write . && eslint --fix",
+ "check-types": "tsc --noEmit"
},
"dependencies": {
- "@base-ui/react": "^1.1.0",
+ "@base-ui/react": "^1.8.0",
"@daveyplate/better-auth-tanstack": "^1.3.6",
- "@daveyplate/better-auth-ui": "^3.3.15",
- "@fontsource-variable/inter": "^5.2.8",
- "@tailwindcss/vite": "^4.0.6",
- "@tanstack/react-devtools": "^0.7.0",
- "@tanstack/react-query": "^5.90.18",
- "@tanstack/react-router": "^1.132.0",
- "@tanstack/react-router-devtools": "^1.132.0",
- "@tanstack/react-router-ssr-query": "^1.131.7",
- "@tanstack/react-start": "^1.132.0",
- "@tanstack/router-plugin": "^1.132.0",
- "axios": "^1.13.2",
- "better-auth": "^1.4.14",
+ "@daveyplate/better-auth-ui": "^3.4.0",
+ "@fontsource-variable/inter": "^5.3.0",
+ "@repo/auth-ui": "workspace:*",
+ "@repo/config": "workspace:*",
+ "@repo/onboarding": "workspace:*",
+ "@tailwindcss/vite": "^4.3.3",
+ "@tanstack/react-devtools": "^0.10.12",
+ "@tanstack/react-query": "^5.102.8",
+ "@tanstack/react-router": "^1.170.32",
+ "@tanstack/react-router-devtools": "^1.167.1",
+ "@tanstack/react-router-ssr-query": "^1.167.2",
+ "@tanstack/react-start": "^1.168.49",
+ "@tanstack/router-plugin": "^1.168.35",
+ "axios": "^1.20.0",
+ "better-auth": "^1.7.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "lucide-react": "^0.562.0",
+ "lucide-react": "^1.41.0",
"next-themes": "^0.4.6",
- "nitro": "latest",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "shadcn": "^3.7.0",
- "sonner": "^2.0.7",
- "tailwind-merge": "^3.4.0",
- "tailwindcss": "^4.0.6",
+ "nitro": "3.0.260903-beta",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "shadcn": "^4.21.0",
+ "sonner": "^2.0.8",
+ "tailwind-merge": "^3.6.0",
+ "tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0",
- "vite-tsconfig-paths": "^5.1.4"
+ "vite-tsconfig-paths": "^6.1.1"
},
"devDependencies": {
- "@tanstack/devtools-vite": "^0.3.11",
- "@tanstack/eslint-config": "^0.3.0",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/react": "^16.2.0",
- "@types/node": "^22.10.2",
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "@vitejs/plugin-react": "^5.0.4",
- "jsdom": "^27.0.0",
- "prettier": "^3.5.3",
- "typescript": "^5.7.2",
- "vite": "^7.1.7",
- "vitest": "^3.0.5",
- "web-vitals": "^5.1.0"
+ "@tanstack/devtools-vite": "^0.8.5",
+ "@tanstack/eslint-config": "^0.4.0",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.3",
+ "@types/node": "^24.0.0",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.7",
+ "@vitejs/plugin-react": "^6.1.1",
+ "jsdom": "^30.0.1",
+ "prettier": "^3.9.6",
+ "typescript": "~6.0.3",
+ "vite": "^8.2.2",
+ "vitest": "^5.0.0",
+ "web-vitals": "^6.2.1"
}
}
diff --git a/apps/tanstack/src/components/auth/ProtectedRoute.tsx b/apps/tanstack/src/components/auth/ProtectedRoute.tsx
deleted file mode 100644
index 7c6c2e6..0000000
--- a/apps/tanstack/src/components/auth/ProtectedRoute.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-// ** import types
-import type { ReactNode } from 'react'
-
-// ** import lib
-import { SignedIn, SignedOut } from '@daveyplate/better-auth-ui'
-import { useRouter } from '@tanstack/react-router'
-import { useEffect } from 'react'
-
-interface ProtectedRouteProps {
- children: ReactNode
- redirectTo?: string
-}
-
-export function ProtectedRoute({
- children,
- redirectTo = '/auth/sign-in',
-}: ProtectedRouteProps) {
- return (
- <>
-
{text}
+Invalid invitation link.
+ )} ++ Open the verification link sent to{' '} + + {email || 'your inbox'} + {' '} + to continue. +
++ {stepConfig?.description} +
+{text}
++ {stepConfig?.description} +
++ Open the verification link sent to{" "} + + {email || "your inbox"} + {" "} + to continue. +
+Invalid invitation link.
+ )} +This is protected content.
+Welcome back!
+ Go to Dashboard +Please sign in to continue.
+ Sign In +Loading...
; + if (!session) returnNot logged in
; + + return ( +Name: {session.user.name}
+Email: {session.user.email}
+Accepting invitation...
; + } + + if (status === "success") { + return ( +Redirecting to dashboard...
+No pending invitations
; + } + + return ( +Loading...
+ ) : ( +Welcome, {session?.user?.name || "User"}!
+Email: {session?.user?.email}
+This content is only visible to logged-in users.
+Unable to verify your session.
+ + > + ) : ( +Loading...
+ )} ++ You’re invited to {context.organizationName}. +
+ )} ++ {error} +
+ )} + {notice && ( ++ {notice} +
+ )} + {!context ? ( ++ {error + ? "Ask the organization owner for a new invitation." + : "Loading invitation…"} +
+ ) : session ? ( + session.user.email.toLowerCase() !== context.email.toLowerCase() ? ( + <> ++ This invitation is for {context.email}. Sign out to use that + account. +
+ + > + ) : !session.user.emailVerified ? ( + <> ++ Verify {context.email} using the link in your email, then continue + here. +
++ {error} +
+ )} +