Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/start/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@
"label": "Basic + Supabase Auth",
"to": "framework/react/examples/start-supabase-basic"
},
{
"label": "Basic + Appwrite Auth",
"to": "framework/react/examples/start-appwrite-basic"
},
{
"label": "Trellaux + Convex",
"to": "framework/react/examples/start-convex-trellaux"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Build your own authentication system using TanStack Start's server functions and

- [Basic Auth with Prisma](https://github.com/TanStack/router/tree/main/examples/react/start-basic-auth)
- [Supabase Auth](https://github.com/TanStack/router/tree/main/examples/react/start-supabase-basic)
- [Appwrite Auth](https://github.com/TanStack/router/tree/main/examples/react/start-appwrite-basic)

**Client-Side Examples:**

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:24"
}
3 changes: 3 additions & 0 deletions examples/react/start-appwrite-basic/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=PleaseChangeMe
APPWRITE_API_KEY=PleaseChangeMe
20 changes: 20 additions & 0 deletions examples/react/start-appwrite-basic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
node_modules
# Keep environment variables out of version control
!.env
package-lock.json
yarn.lock

.DS_Store
.cache
.env
.vercel
.output
/build/
/api/
/server/build
/public/build# Sentry Config File
.env.sentry-build-plugin
Comment on lines +15 to +16
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Split the /public/build ignore rule from the Sentry comment.

Line 15 currently ignores a literal /public/build# Sentry Config File path, so /public/build is not covered.

Proposed fix
-/public/build# Sentry Config File
+/public/build
+# Sentry Config File
 .env.sentry-build-plugin
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/public/build# Sentry Config File
.env.sentry-build-plugin
/public/build
# Sentry Config File
.env.sentry-build-plugin
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-appwrite-basic/.gitignore` around lines 15 - 16, The
ignore line currently contains the literal "/public/build# Sentry Config File"
so the /public/build path isn't actually ignored; change the single line that
contains "/public/build# Sentry Config File" into two lines by separating the
path and the comment: add a line with "/public/build" and on the next line add
the comment beginning with "#" (e.g., "# Sentry Config File"), leaving the
existing ".env.sentry-build-plugin" entry unchanged.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherited verbatim from examples/react/start-supabase-basic/.gitignore. Keeping parity with the existing example here; happy to do a follow-up PR that fixes the malformed line in both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChiragAgg5k, understood — keeping the two examples in sync makes sense. Would you like me to open a follow-up GitHub issue to track fixing the malformed /public/build# Sentry Config File line in both examples/react/start-supabase-basic/.gitignore and examples/react/start-appwrite-basic/.gitignore? That way it won't get lost.

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
4 changes: 4 additions & 0 deletions examples/react/start-appwrite-basic/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/build
**/public
pnpm-lock.yaml
routeTree.gen.ts
11 changes: 11 additions & 0 deletions examples/react/start-appwrite-basic/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}
60 changes: 60 additions & 0 deletions examples/react/start-appwrite-basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# TanStack Start - Appwrite Example

A TanStack Start example demonstrating integration with [Appwrite](https://appwrite.io) for authentication.

- [TanStack Router Docs](https://tanstack.com/router)
- [Appwrite Documentation](https://appwrite.io/docs)

## Start a new project based on this example

To start a new project based on this example, run:

```sh
npx gitpick TanStack/router/tree/main/examples/react/start-appwrite-basic start-appwrite-basic
```

## Setup

This example uses Appwrite's [Server-Side Rendering flow](https://appwrite.io/docs/products/auth/server-side-rendering)
with the [`node-appwrite`](https://www.npmjs.com/package/node-appwrite) SDK. The `.env` file
contains the environment variables the server needs:

```env
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=your-project-id
APPWRITE_API_KEY=your-server-api-key
```

You'll need to:

1. Create an Appwrite project at [cloud.appwrite.io](https://cloud.appwrite.io) (or self-host)
2. Enable the **Email/Password** authentication method in **Auth → Settings**
3. Create a server **API Key** with at least the `sessions.write` and `users.write` scopes
4. Copy the project ID and API key into the `.env` file

## Getting Started

From your terminal:

```sh
pnpm install
pnpm dev
```

This starts your app in development mode, rebuilding assets on file changes.

## Build

To build the app for production:

```sh
pnpm build
```

## Appwrite Integration

This example demonstrates:

- Email/password sign up and sign in with Appwrite's Account service
- SSR-safe auth state using a session-secret cookie read on the server
- Server functions (`createServerFn`) that use the SSR `node-appwrite` SDK
33 changes: 33 additions & 0 deletions examples/react/start-appwrite-basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "tanstack-start-example-appwrite-basic",
"private": true,
"main": "index.js",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@tanstack/react-router": "^1.168.23",
"@tanstack/react-router-devtools": "^1.166.13",
"@tanstack/react-start": "^1.167.42",
"node-appwrite": "^24.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"redaxios": "^0.5.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2",
"vite": "^8.0.0"
}
}
57 changes: 57 additions & 0 deletions examples/react/start-appwrite-basic/src/components/Auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export function Auth({
actionText,
onSubmit,
status,
afterSubmit,
}: {
actionText: string
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
status: 'pending' | 'idle' | 'success' | 'error'
afterSubmit?: React.ReactNode
}) {
return (
<div className="fixed inset-0 bg-white dark:bg-black flex items-start justify-center p-8">
<div className="bg-white dark:bg-gray-900 p-8 rounded-lg shadow-lg">
<h1 className="text-2xl font-bold mb-4">{actionText}</h1>
<form
onSubmit={(e) => {
e.preventDefault()
onSubmit(e)
}}
className="space-y-4"
>
<div>
<label htmlFor="email" className="block text-xs">
Username
</label>
<input
type="email"
name="email"
id="email"
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
/>
</div>
<div>
<label htmlFor="password" className="block text-xs">
Password
</label>
<input
type="password"
name="password"
id="password"
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
Comment on lines +24 to +42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the auth fields match email/password semantics.

Line 25 labels an email field as “Username”, and both fields allow empty submission. This makes the example easier to misuse and pushes avoidable validation errors to the server.

Suggested form field cleanup
             <label htmlFor="email" className="block text-xs">
-              Username
+              Email
             </label>
             <input
               type="email"
               name="email"
               id="email"
+              required
+              autoComplete="email"
               className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
             />
@@
             <input
               type="password"
               name="password"
               id="password"
+              required
               className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
             />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label htmlFor="email" className="block text-xs">
Username
</label>
<input
type="email"
name="email"
id="email"
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
/>
</div>
<div>
<label htmlFor="password" className="block text-xs">
Password
</label>
<input
type="password"
name="password"
id="password"
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
<label htmlFor="email" className="block text-xs">
Email
</label>
<input
type="email"
name="email"
id="email"
required
autoComplete="email"
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
/>
</div>
<div>
<label htmlFor="password" className="block text-xs">
Password
</label>
<input
type="password"
name="password"
id="password"
required
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-appwrite-basic/src/components/Auth.tsx` around lines 24
- 42, Update the auth form in the Auth component so the fields match
email/password semantics: change the label text for the input with
id="email"/name="email" from "Username" to "Email", add required attributes to
both the email and password inputs (and consider adding autoComplete="email" and
autoComplete="current-password"), and add simple client-side validation (e.g.,
input type="email" already enforces format; ensure the password input has
required and a sensible minLength) on the inputs with id="email" and
id="password" so empty submissions are prevented.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherited verbatim from examples/react/start-supabase-basic/src/components/Auth.tsx. Keeping parity with the existing example (including the quirky "Username" label on an email input); happy to upgrade both in a follow-up PR if maintainers want the tighter form semantics everywhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChiragAgg5k That makes total sense — keeping the two examples structurally identical makes diffs between them much cleaner for learners. I'll defer to the maintainers on whether a follow-up cleanup pass across both examples is worthwhile.

Would you like me to open a GitHub issue to track the form-field improvements ("Email" label, required, autoComplete) for both start-supabase-basic and start-appwrite-basic so it doesn't get lost?

ʕ •ᴥ•ʔ


✏️ Learnings added
Learnt from: ChiragAgg5k
Repo: TanStack/router PR: 7225
File: examples/react/start-appwrite-basic/src/components/Auth.tsx:24-42
Timestamp: 2026-04-19T10:49:35.910Z
Learning: In the TanStack Router repository, `examples/react/start-appwrite-basic/src/components/Auth.tsx` is intentionally copied verbatim from `examples/react/start-supabase-basic/src/components/Auth.tsx` to maintain parity between the two auth examples. Any improvements (e.g. label text, `required`, `autoComplete` attributes) should be applied to both files together in a follow-up PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

/>
</div>
<button
type="submit"
className="w-full bg-cyan-600 text-white rounded-sm py-2 font-black uppercase"
disabled={status === 'pending'}
>
{status === 'pending' ? '...' : actionText}
</button>
{afterSubmit ? afterSubmit : null}
</form>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react'
import {
ErrorComponent,
Link,
rootRouteId,
useMatch,
useRouter,
} from '@tanstack/react-router'
import type { ErrorComponentProps } from '@tanstack/react-router'

export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
const router = useRouter()
const isRoot = useMatch({
strict: false,
select: (state) => state.id === rootRouteId,
})

console.error(error)

return (
<div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6">
<ErrorComponent error={error} />
<div className="flex gap-2 items-center flex-wrap">
<button
onClick={() => {
router.invalidate()
}}
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
>
Try Again
</button>
{isRoot ? (
<Link
to="/"
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
>
Home
</Link>
) : (
<Link
to="/"
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
onClick={(e) => {
e.preventDefault()
window.history.back()
}}
>
Go Back
</Link>
)}
</div>
</div>
)
}
72 changes: 72 additions & 0 deletions examples/react/start-appwrite-basic/src/components/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useRouter } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { useMutation } from '../hooks/useMutation'
import { loginFn } from '../routes/_authed'
import { signupFn } from '../routes/signup'
import { Auth } from './Auth'

export function Login() {
const router = useRouter()

const loginMutation = useMutation({
fn: loginFn,
onSuccess: async (ctx) => {
if (!ctx.data?.error) {
await router.invalidate()
router.navigate({ to: '/' })
return
}
},
})

const signupMutation = useMutation({
fn: useServerFn(signupFn),
})
Comment on lines +11 to +24
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

TanStack Start useServerFn when calling server functions from client components

💡 Result:

In TanStack Start, use useServerFn when calling server functions (created with createServerFn) from client components. This hook wraps the server function in a useCallback that handles server responses properly on the client, including automatic handling of errors, redirects (via throw redirect), and other server-specific responses that would not work correctly if calling the raw server function directly from client code. Example usage in a client component with TanStack Query: function PostList { const getPosts = useServerFn(getServerPosts) const { data } = useQuery({ queryKey: ['posts'], queryFn: => getPosts, }) // ... } Or with mutations: const addPlaylistFn = useMutation({ mutationFn: useServerFn(addPlaylist), }) Without useServerFn, redirects and certain error responses from server functions fail silently or incorrectly on the client. Server functions can be called directly from server contexts like route loaders without this hook.

Citations:


🏁 Script executed:

cat -n examples/react/start-appwrite-basic/src/components/Login.tsx | head -40

Repository: TanStack/router

Length of output: 1354


🏁 Script executed:

find examples/react/start-appwrite-basic/src/routes -name '*authed*' -o -name 'signup*' | head -10

Repository: TanStack/router

Length of output: 230


🏁 Script executed:

cat -n examples/react/start-appwrite-basic/src/routes/_authed.tsx

Repository: TanStack/router

Length of output: 1395


🏁 Script executed:

cat -n examples/react/start-appwrite-basic/src/routes/signup.tsx

Repository: TanStack/router

Length of output: 2480


Wrap loginFn with useServerFn—required for proper error and redirect handling.

Both loginFn and signupFn are server functions created with createServerFn. When calling server functions from client components in TanStack Start, useServerFn is required to properly handle server responses, including error states and redirects. signupFn correctly uses useServerFn (and relies on it due to the throw redirect() on line 36 of signup.tsx), but loginFn is called directly and bypasses this wrapper. Without it, error handling and server-side redirects fail silently or incorrectly on the client.

Fix
   const loginMutation = useMutation({
-    fn: loginFn,
+    fn: useServerFn(loginFn),
     onSuccess: async (ctx) => {
       if (!ctx.data?.error) {
         await router.invalidate()
         router.navigate({ to: '/' })
         return
       }
     },
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const loginMutation = useMutation({
fn: loginFn,
onSuccess: async (ctx) => {
if (!ctx.data?.error) {
await router.invalidate()
router.navigate({ to: '/' })
return
}
},
})
const signupMutation = useMutation({
fn: useServerFn(signupFn),
})
const loginMutation = useMutation({
fn: useServerFn(loginFn),
onSuccess: async (ctx) => {
if (!ctx.data?.error) {
await router.invalidate()
router.navigate({ to: '/' })
return
}
},
})
const signupMutation = useMutation({
fn: useServerFn(signupFn),
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-appwrite-basic/src/components/Login.tsx` around lines 11
- 24, The loginMutation currently calls loginFn directly which bypasses Start's
client wrapper—wrap loginFn with useServerFn (same pattern used for
signupMutation) so server responses, errors and redirects are handled correctly;
update the loginMutation's fn to useServerFn(loginFn) (keeping useMutation and
onSuccess logic intact) to match how signupMutation uses useServerFn.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start-supabase-basic's Login passes loginFn directly too (fn: loginFn on its useMutation), and it works because the login server function does not throw redirect() — it only returns {error, message} and the component handles navigation itself via router.navigate in onSuccess. Keeping parity here; the signupFn path (which does throw redirect()) correctly uses useServerFn. Happy to upgrade both examples to wrap loginFn as well in a follow-up if maintainers prefer the stricter pattern.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChiragAgg5k, that's a fair and accurate clarification — thank you. Since loginFn only returns { error, message } and never throw redirect(), calling it directly is safe and the component correctly handles navigation client-side via router.navigate. useServerFn is the strict requirement only when the server function uses throw redirect() (as signupFn does). Parity with start-supabase-basic makes sense here. I'll withdraw the concern.


✏️ Learnings added
Learnt from: ChiragAgg5k
Repo: TanStack/router PR: 7225
File: examples/react/start-appwrite-basic/src/components/Login.tsx:11-24
Timestamp: 2026-04-19T10:49:38.189Z
Learning: In TanStack Start examples (e.g., `examples/react/start-appwrite-basic` and `examples/react/start-supabase-basic`), `useServerFn` is only required when a server function uses `throw redirect()` (e.g., `signupFn`). Server functions that only return plain data objects like `{ error: true, message }` (e.g., `loginFn` in `_authed.tsx`) can be passed directly to `useMutation` as `fn: loginFn` without `useServerFn`, and the component handles navigation itself via `router.navigate` in `onSuccess`. This is an intentional pattern maintained for parity between examples.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hokkyss
Repo: TanStack/router PR: 6394
File: e2e/react-start/server-functions/src/routes/function-method/index.tsx:93-95
Timestamp: 2026-01-17T08:01:35.654Z
Learning: In TanStack Router e2e server function test files (e.g., files in `e2e/react-start/server-functions`, `e2e/solid-start/server-functions`, `e2e/vue-start/server-functions`), unhandled promise rejections in server function calls (e.g., `fn().then(setResult)` without `.catch()`) are intentional test behavior to make test failures visible in the test runner, consistent with existing patterns like in `e2e/react-start/server-functions/src/routes/factory/index.tsx`.


return (
<Auth
actionText="Login"
status={loginMutation.status}
onSubmit={(e) => {
const formData = new FormData(e.target as HTMLFormElement)

loginMutation.mutate({
data: {
email: formData.get('email') as string,
password: formData.get('password') as string,
},
})
}}
afterSubmit={
loginMutation.data ? (
<>
<div className="text-red-400">{loginMutation.data.message}</div>
{loginMutation.data.error &&
/invalid credentials/i.test(loginMutation.data.message) ? (
<div>
<button
className="text-blue-500"
onClick={(e) => {
const formData = new FormData(
(e.target as HTMLButtonElement).form!,
)

signupMutation.mutate({
data: {
email: formData.get('email') as string,
password: formData.get('password') as string,
},
})
}}
type="button"
>
Sign up instead?
</button>
</div>
) : null}
</>
) : null
}
/>
)
}
25 changes: 25 additions & 0 deletions examples/react/start-appwrite-basic/src/components/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Link } from '@tanstack/react-router'

export function NotFound({ children }: { children?: React.ReactNode }) {
return (
<div className="space-y-2 p-2">
<div className="text-gray-600 dark:text-gray-400">
{children || <p>The page you are looking for does not exist.</p>}
</div>
<p className="flex items-center gap-2 flex-wrap">
<button
onClick={() => window.history.back()}
className="bg-emerald-500 text-white px-2 py-1 rounded-sm uppercase font-black text-sm"
>
Go back
</button>
<Link
to="/"
className="bg-cyan-600 text-white px-2 py-1 rounded-sm uppercase font-black text-sm"
>
Start Over
</Link>
</p>
</div>
)
}
Loading