feat: remove README and cursor rules, update dependencies, and implement authentication middleware

This commit is contained in:
2026-05-23 14:30:25 +02:00
parent 5b02cff4b9
commit b35da71363
11 changed files with 190 additions and 334 deletions

View File

@@ -1,22 +0,0 @@
We use Sentry for watching for errors in our deployed application, as well as for instrumentation of our application.
## Error collection
Error collection is automatic and configured in `src/router.tsx`.
## Instrumentation
We want our server functions instrumented. So if you see a function name like `createServerFn`, you can instrument it with Sentry. You'll need to import `Sentry`:
```tsx
import * as Sentry from '@sentry/tanstackstart-react'
```
And then wrap the implementation of the server function with `Sentry.startSpan`, like so:
```tsx
Sentry.startSpan({ name: 'Requesting all the pokemon' }, async () => {
// Some lengthy operation here
await fetch('https://api.pokemon.com/data/')
})
```

51
CLAUDE.md Normal file
View File

@@ -0,0 +1,51 @@
# CLAUDE.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
1. Think Before Coding
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
* State your assumptions explicitly. If uncertain, ask.
* If multiple interpretations exist, present them - don't pick silently.
* If a simpler approach exists, say so. Push back when warranted.
* If something is unclear, stop. Name what's confusing. Ask.
2. Simplicity First
Minimum code that solves the problem. Nothing speculative.
* No features beyond what was asked.
* No abstractions for single-use code.
* No "flexibility" or "configurability" that wasn't requested.
* No error handling for impossible scenarios.
* If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
3. Surgical Changes
Touch only what you must. Clean up only your own mess.
When editing existing code:
* Don't "improve" adjacent code, comments, or formatting.
* Don't refactor things that aren't broken.
* Match existing style, even if you'd do it differently.
* If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
* Remove imports/variables/functions that YOUR changes made unused.
* Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
4. Goal-Driven Execution
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
* "Add validation" → "Write tests for invalid inputs, then make them pass"
* "Fix the bug" → "Write a test that reproduces it, then make it pass"
* "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

222
README.md
View File

@@ -1,222 +0,0 @@
Welcome to your new TanStack Start app!
# Getting Started
To run this application:
```bash
npm install
npm run dev
```
# Building For Production
To build this application for production:
```bash
npm run build
```
## Testing
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
```bash
npm run test
```
## Styling
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
### Removing Tailwind CSS
If you prefer not to use Tailwind CSS:
1. Remove the demo pages in `src/routes/demo/`
2. Replace the Tailwind import in `src/styles.css` with your own styles
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D`
## Linting & Formatting
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
```bash
npm run lint
npm run format
npm run check
```
## Shadcn
Add components using the latest version of [Shadcn](https://ui.shadcn.com/).
```bash
pnpm dlx shadcn@latest add button
```
# Paraglide i18n
This add-on wires up ParaglideJS for localized routing and message formatting.
- Messages live in `project.inlang/messages`.
- URLs are localized through the Paraglide Vite plugin and router `rewrite` hooks.
- Run the dev server or build to regenerate the `src/paraglide` outputs.
## Routing
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
### Adding A Route
To add a new route to your application just add a new file in the `./src/routes` directory.
TanStack will automatically generate the content of the route file for you.
Now that you have two routes you can use a `Link` component to navigate between them.
### Adding Links
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
```tsx
import { Link } from "@tanstack/react-router";
```
Then anywhere in your JSX you can use it like so:
```tsx
<Link to="/about">About</Link>
```
This will create a link that will navigate to the `/about` route.
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
### Using A Layout
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
Here is an example layout that includes a header:
```tsx
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'My App' },
],
}),
shellComponent: ({ children }) => (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
</header>
{children}
<Scripts />
</body>
</html>
),
})
```
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
## Server Functions
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
```tsx
import { createServerFn } from '@tanstack/react-start'
const getServerTime = createServerFn({
method: 'GET',
}).handler(async () => {
return new Date().toISOString()
})
// Use in a component
function MyComponent() {
const [time, setTime] = useState('')
useEffect(() => {
getServerTime().then(setTime)
}, [])
return <div>Server time: {time}</div>
}
```
## API Routes
You can create API routes by using the `server` property in your route definitions:
```tsx
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: () => json({ message: 'Hello, World!' }),
},
},
})
```
## Data Fetching
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
For example:
```tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/people')({
loader: async () => {
const response = await fetch('https://swapi.dev/api/people')
return response.json()
},
component: PeopleComponent,
})
function PeopleComponent() {
const data = Route.useLoaderData()
return (
<ul>
{data.results.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
)
}
```
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
# Demo files
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
# Learn More
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).

View File

@@ -14,45 +14,45 @@
"machine-translate": "inlang machine translate --project project.inlang"
},
"dependencies": {
"@heroui/react": "^3.0.0-rc.1",
"@heroui/styles": "^3.0.0-rc.1",
"@heroui/react": "^3.0.5",
"@heroui/styles": "^3.0.5",
"@sentry/tanstackstart-react": "^10.45.0",
"@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.99.3",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.91.2",
"@tanstack/react-router": "^1.167.5",
"@tanstack/react-router-ssr-query": "^1.166.9",
"@tanstack/react-start": "^1.166.17",
"@tanstack/router-plugin": "^1.166.14",
"@supabase/ssr": "^0.10.3",
"@supabase/supabase-js": "^2.106.1",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-query": "^5.100.11",
"@tanstack/react-router": "^1.170.7",
"@tanstack/react-router-ssr-query": "^1.167.0",
"@tanstack/react-start": "^1.168.10",
"@tanstack/router-plugin": "^1.168.10",
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
"maplibre-gl": "^5.20.2",
"nitro": "^3.0.1-alpha.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"lucide-react": "^1.16.0",
"maplibre-gl": "^5.24.0",
"nitro": "^3.0.260522-beta",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"zod": "^4.3.6"
"zod": "^4.4.3"
},
"devDependencies": {
"@biomejs/biome": "^2.4.8",
"@inlang/paraglide-js": "^2.15.0",
"@tanstack/devtools-vite": "^0.6.0",
"@tanstack/react-devtools": "^0.10.0",
"@tanstack/react-query-devtools": "^5.91.3",
"@tanstack/react-router-devtools": "^1.166.9",
"@biomejs/biome": "^2.4.15",
"@inlang/paraglide-js": "^2.18.1",
"@tanstack/devtools-vite": "^0.7.0",
"@tanstack/react-devtools": "^0.10.5",
"@tanstack/react-query-devtools": "^5.100.11",
"@tanstack/react-router-devtools": "^1.167.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^22.19.15",
"@types/react": "^19.2.14",
"@types/node": "^22.19.19",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^29.0.0",
"typescript": "^5.9.3",
"vite": "^8.0.1",
"@vitejs/plugin-react": "^6.0.2",
"jsdom": "^29.1.1",
"typescript": "^6.0.3",
"vite": "^8.0.14",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.0"
"vitest": "^4.1.7"
}
}

View File

@@ -19,8 +19,8 @@ export function getSupabaseServerClient() {
}))
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value)
cookies.forEach(({ name, value, options }) => {
setCookie(name, value, options)
})
}
}

View File

@@ -0,0 +1,45 @@
import { createMiddleware } from "@tanstack/react-start"
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
export const authMiddleware = createMiddleware().server(async ({ next }) => {
const supabase = getSupabaseServerClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data.user) {
throw new Error("Unauthorized")
}
return next({
context: {
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata?.name || "",
location: data.user.user_metadata?.location || ""
}
}
})
})
export const optionalAuthMiddleware = createMiddleware().server(
async ({ next }) => {
const supabase = getSupabaseServerClient()
const { data, error } = await supabase.auth.getUser()
const userData =
!error && data.user
? {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata?.name || "",
location: data.user.user_metadata?.location || ""
}
: null
return next({
context: {
user: userData
}
})
}
)

View File

@@ -4,8 +4,9 @@ import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
import {
loginFormSchema,
signupFormSchema,
userListParamsSchema,
} from "../validation/user"
userListParamsSchema
} from "@/lib/validation/user"
import { authMiddleware, optionalAuthMiddleware } from "./middleware"
const login = createServerFn({ method: "POST" })
.inputValidator(loginFormSchema)
@@ -14,38 +15,41 @@ const login = createServerFn({ method: "POST" })
const login = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password,
password: data.password
})
if (login.error) {
return {
error: true,
message: login.error.message,
message: login.error.message
}
}
return {
error: false,
}
throw redirect({
to: "/dashboard",
replace: true
})
})
const logout = createServerFn().handler(async () => {
const supabase = getSupabaseServerClient()
const logout = createServerFn()
.middleware([authMiddleware])
.handler(async () => {
const supabase = getSupabaseServerClient()
const { error } = await supabase.auth.signOut()
if (error) {
return {
error: true,
message: error.message,
const { error } = await supabase.auth.signOut()
if (error) {
return {
error: true,
message: error.message
}
}
}
throw redirect({
to: "/",
viewTransition: true,
replace: true,
throw redirect({
to: "/",
viewTransition: true,
replace: true
})
})
})
const signup = createServerFn({ method: "POST" })
.inputValidator(signupFormSchema)
@@ -57,41 +61,36 @@ const signup = createServerFn({ method: "POST" })
options: {
data: {
name: data.name,
location: data.location,
},
},
location: data.location
}
}
})
if (error) {
return {
error: true,
message: error.message,
message: error.message
}
}
throw redirect({
href: data.redirectUrl || "/",
href: data.redirectUrl || "/"
})
})
const userData = createServerFn().handler(async () => {
const supabase = getSupabaseServerClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data.user) {
return {
error: true,
message: error?.message ?? "Unknown error",
const userData = createServerFn()
.middleware([optionalAuthMiddleware])
.handler(async ({ context }) => {
if (!context.user) {
return {
error: true,
message: "Not authenticated"
}
}
}
return {
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata.name || "",
location: data.user.user_metadata.location || "",
},
error: false,
}
})
return {
user: context.user,
error: false
}
})
const resendConfirmationEmail = createServerFn({ method: "POST" })
.inputValidator(signupFormSchema.pick({ email: true }))
@@ -102,33 +101,34 @@ const resendConfirmationEmail = createServerFn({ method: "POST" })
if (error) {
return {
error: true,
message: error.message,
message: error.message
}
}
return {
error: false,
error: false
}
})
const userList = createServerFn()
.middleware([authMiddleware])
.inputValidator(userListParamsSchema)
.handler(async ({ data }) => {
const supabase = getSupabaseServerClient()
const users = await supabase.auth.admin.listUsers({
page: data.page,
perPage: data.limit,
perPage: data.limit
})
if (users.error) {
return {
error: true,
message: users.error.message,
message: users.error.message
}
}
return {
users: users.data,
error: false,
error: false
}
})
@@ -138,5 +138,5 @@ export const user = {
signup,
userData,
resendConfirmationEmail,
userList,
userList
}

View File

@@ -12,7 +12,7 @@ import appCss from "@/styles/globals.css?url"
interface MyRouterContext {
queryClient: QueryClient
user: null
user: Awaited<ReturnType<typeof user.userData>>
}
export const Route = createRootRouteWithContext<MyRouterContext>()({

View File

@@ -1,9 +1,13 @@
import { createFileRoute, Link, Outlet } from "@tanstack/react-router"
import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router"
export const Route = createFileRoute("/_auth")({
beforeLoad: ({ context }) => {
if (context.user.error) {
throw new Error("Not authenticated")
redirect({
to: "/access/login",
replace: true,
throw: true
})
}
},
errorComponent: ({ error }) => {

View File

@@ -4,7 +4,6 @@
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},

View File

@@ -5,20 +5,21 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import viteReact from "@vitejs/plugin-react"
import { nitro } from "nitro/vite"
import { defineConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
export default defineConfig({
plugins: [
devtools(),
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/paraglide",
outdir: "./src/integrations/paraglide",
strategy: ["url", "baseLocale"]
}),
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
tsconfigPaths({ projects: ["./tsconfig.json"] }),
tailwindcss(),
tanstackStart(),
viteReact()
]
],
resolve: {
tsconfigPaths: true
}
})