Example: Add example with Appwrite Auth#7225
Example: Add example with Appwrite Auth#7225ChiragAgg5k wants to merge 2 commits intoTanStack:mainfrom
Conversation
Mirrors the start-supabase-basic example but uses Appwrite for email/password authentication. Adds a React Start example that demonstrates the canonical Appwrite server-side rendering flow: - session secret stored in an httpOnly `appwrite-session` cookie - `createSessionClient` / `createAdminClient` helpers over `node-appwrite` - server functions for login, signup, logout and a root `fetchUser` - `_authed` layout route that gates `/posts` and renders the Login form as its error component when the user is not authenticated - docs/start/config.json + docs/start/framework/react/guide/authentication-overview.md updated to surface the new example alongside start-supabase-basic
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds a complete React example demonstrating TanStack Start + Appwrite authentication: project scaffolding, routing, server functions for auth, protected routes, UI components, Appwrite utilities, data-fetching examples, configuration, and documentation. Changes
Sequence DiagramssequenceDiagram
rect rgba(220, 235, 255, 0.5)
actor User
end
participant Client as React Client
participant Server as TanStack Start (Server Fns)
participant Appwrite as Appwrite Backend
User->>Client: Submit login/signup form
Client->>Server: POST email & password (server fn)
Server->>Appwrite: createEmailPasswordSession(email,password)
alt Success
Appwrite-->>Server: Session created
Server->>Server: set session cookie (session-secret)
Server-->>Client: { error: false }
Client->>Client: Invalidate cache & navigate home
else Failure
Appwrite-->>Server: Error (invalid credentials)
Server-->>Client: { error: true, message }
Client->>Client: Show error, offer signup
end
sequenceDiagram
participant Client as React Client
participant Server as TanStack Start (beforeLoad)
participant Appwrite as Appwrite Backend
Client->>Server: Navigate to protected route (/_authed)
Server->>Appwrite: account.get() using session cookie
alt Valid session
Appwrite-->>Server: { email: "..." }
Server->>Server: Attach user to route context
Server-->>Client: Render protected content
else No session / invalid
Appwrite-->>Server: Error / null
Server-->>Client: Throw "Not authenticated"
Client->>Client: Render Login component
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
examples/react/start-appwrite-basic/src/utils/appwrite.ts (1)
10-18: Replace env non-null assertions with runtime validation.The
!assertions hide missing Appwrite config from TypeScript and defer failure to opaque SDK errors. Fail fast with explicit messages. As per coding guidelines,**/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.Proposed refactor
+function requiredEnv(name: 'APPWRITE_PROJECT_ID' | 'APPWRITE_API_KEY') { + const value = process.env[name] + if (!value) { + throw new Error(`${name} is required`) + } + return value +} + function getEndpoint() { - return process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1' + return process.env.APPWRITE_ENDPOINT || 'https://cloud.appwrite.io/v1' } export function createSessionClient() { const client = new Client() .setEndpoint(getEndpoint()) - .setProject(process.env.APPWRITE_PROJECT_ID!) + .setProject(requiredEnv('APPWRITE_PROJECT_ID')) @@ export function createAdminClient() { const client = new Client() .setEndpoint(getEndpoint()) - .setProject(process.env.APPWRITE_PROJECT_ID!) - .setKey(process.env.APPWRITE_API_KEY!) + .setProject(requiredEnv('APPWRITE_PROJECT_ID')) + .setKey(requiredEnv('APPWRITE_API_KEY'))Also applies to: 31-35
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/start-appwrite-basic/src/utils/appwrite.ts` around lines 10 - 18, The code uses non-null assertions for Appwrite config (process.env.APPWRITE_PROJECT_ID) in createSessionClient (and similarly where APPWRITE_PROJECT_ID is used around lines 31-35); replace the `!` with runtime validation: read process.env.APPWRITE_PROJECT_ID into a const, check if it is undefined or empty, and throw a clear, descriptive Error (e.g., "APPWRITE_PROJECT_ID is required") before constructing the Client in createSessionClient (and apply the same check where the variable is used elsewhere) so TypeScript strictness is preserved and failures fail fast with actionable messages.examples/react/start-appwrite-basic/src/router.tsx (1)
1-17: Consider wiringDefaultCatchBoundaryas the router's default error component.
DefaultCatchBoundaryis defined insrc/components/DefaultCatchBoundary.tsxbut is not referenced fromgetRouter()here. Registering it asdefaultErrorComponent(and optionally adefaultNotFoundComponent) oncreateRouterwould ensure route-level errors are actually caught by it. Skip if this matches the siblingstart-supabase-basictemplate intentionally.Proposed change
- const router = createRouter({ - routeTree, - scrollRestoration: true, - }) + const router = createRouter({ + routeTree, + scrollRestoration: true, + defaultErrorComponent: DefaultCatchBoundary, + })import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/react/start-appwrite-basic/src/router.tsx` around lines 1 - 17, The router currently created in getRouter() doesn't set a default error component, so route-level errors won't be handled by your DefaultCatchBoundary; update the createRouter call inside getRouter to pass defaultErrorComponent: DefaultCatchBoundary (and optionally defaultNotFoundComponent: DefaultCatchBoundary) after importing DefaultCatchBoundary from src/components/DefaultCatchBoundary.tsx so the router uses that component for uncaught route errors and 404s.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/react/start-appwrite-basic/.gitignore`:
- Around line 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.
In `@examples/react/start-appwrite-basic/src/components/Auth.tsx`:
- Around line 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.
In `@examples/react/start-appwrite-basic/src/components/Login.tsx`:
- Around line 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.
In `@examples/react/start-appwrite-basic/src/routes/__root.tsx`:
- Line 104: The root document's <html> tag is missing a lang attribute which is
an accessibility issue; update the <html> element in the root route (the line
that currently contains "<html>") to include an appropriate lang value (e.g.,
lang="en" or the app's primary locale) so screen readers and a11y tools can
detect the document language.
- Line 77: The manifest link entry uses an invalid 5-digit hex '#fffff' and
incorrectly includes a color attribute; remove the color property from the
manifest link object (the entry where rel: 'manifest') and, if a pinned-tab
color was intended, add a separate link with rel: 'mask-icon' and a valid
3/6/8-digit hex (e.g., '#fff' or '#ffffff') for the color attribute so browsers
accept it.
In `@examples/react/start-appwrite-basic/src/routes/signup.tsx`:
- Around line 15-33: The signup flow can leave an orphaned user if
account.create succeeds but account.createEmailPasswordSession fails; update the
handler so after calling account.create (which uses the generated userId) you
wrap the session creation in a try/catch: on session creation failure either (1)
roll back by calling account.delete(userId) to remove the partially-created user
before returning the error, or (2) attempt a deterministic fallback such as
retrying createEmailPasswordSession once and if it still fails return a specific
error flag (e.g., error.code = 'SESSION_CREATION_FAILED') so the caller can
attempt a login path; reference account.create,
account.createEmailPasswordSession, setAppwriteSessionCookie and the generated
userId to implement the rollback or fallback logic.
- Line 1: The named imports are out of alphabetical order on the import from
'@tanstack/react-router'; update the import specifier list so the named exports
are alphabetized (put createFileRoute before redirect) in the import statement
that currently mentions createFileRoute and redirect to satisfy ESLint import
order rules and save.
In `@examples/react/start-appwrite-basic/src/styles/app.css`:
- Around line 27-29: The rule ".using-mouse * { outline: none !important; }"
removes all focus outlines even when keyboard users are navigating; change it to
only remove outline for focus states that are not keyboard-visible (e.g., target
".using-mouse *:focus:not(:focus-visible)" or the equivalent
:focus:not(:focus-visible) selector) so that keyboard-visible focus indicators
remain visible while mouse-only focus still hides outlines.
In `@examples/react/start-appwrite-basic/src/utils/posts.ts`:
- Around line 5-17: The handler in fetchPost currently assumes the external
JSONPlaceholder payload matches PostType (id:string) but the API returns id as a
number; modify the fetch to parse/normalize the response before returning: call
axios.get with an appropriate external response shape (e.g., id:number,
title:string, body:string) or use any, then map the received object to PostType
by converting id to a string (e.g., id: String(response.id)) and copying
title/body; remove the incorrect generic <PostType> on axios.get and ensure the
handler returns the normalized PostType object.
In `@examples/react/start-appwrite-basic/src/utils/seo.ts`:
- Around line 12-30: The tags array in seo.ts currently uses name="og:*" and
always emits description/keywords which can be undefined; update the
construction of the tags array (the tags constant that references title,
description, keywords, image) to: 1) use property: 'og:*' for Open Graph entries
(e.g., property: 'og:title', property: 'og:description', property: 'og:type',
property: 'og:image') instead of name, and 2) conditionally push meta objects
only when their values exist (guard emitting description, keywords, twitter:*
and og:image when description/keywords/image are truthy) so no meta tag has
content: undefined. Ensure references remain to the same variables (title,
description, keywords, image) and preserve existing twitter card logic when
image is present.
---
Nitpick comments:
In `@examples/react/start-appwrite-basic/src/router.tsx`:
- Around line 1-17: The router currently created in getRouter() doesn't set a
default error component, so route-level errors won't be handled by your
DefaultCatchBoundary; update the createRouter call inside getRouter to pass
defaultErrorComponent: DefaultCatchBoundary (and optionally
defaultNotFoundComponent: DefaultCatchBoundary) after importing
DefaultCatchBoundary from src/components/DefaultCatchBoundary.tsx so the router
uses that component for uncaught route errors and 404s.
In `@examples/react/start-appwrite-basic/src/utils/appwrite.ts`:
- Around line 10-18: The code uses non-null assertions for Appwrite config
(process.env.APPWRITE_PROJECT_ID) in createSessionClient (and similarly where
APPWRITE_PROJECT_ID is used around lines 31-35); replace the `!` with runtime
validation: read process.env.APPWRITE_PROJECT_ID into a const, check if it is
undefined or empty, and throw a clear, descriptive Error (e.g.,
"APPWRITE_PROJECT_ID is required") before constructing the Client in
createSessionClient (and apply the same check where the variable is used
elsewhere) so TypeScript strictness is preserved and failures fail fast with
actionable messages.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a578314f-e1db-453d-9640-8da81811795e
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (31)
docs/start/config.jsondocs/start/framework/react/guide/authentication-overview.mdexamples/react/start-appwrite-basic/.devcontainer/devcontainer.jsonexamples/react/start-appwrite-basic/.envexamples/react/start-appwrite-basic/.gitignoreexamples/react/start-appwrite-basic/.prettierignoreexamples/react/start-appwrite-basic/.vscode/settings.jsonexamples/react/start-appwrite-basic/README.mdexamples/react/start-appwrite-basic/package.jsonexamples/react/start-appwrite-basic/src/components/Auth.tsxexamples/react/start-appwrite-basic/src/components/DefaultCatchBoundary.tsxexamples/react/start-appwrite-basic/src/components/Login.tsxexamples/react/start-appwrite-basic/src/components/NotFound.tsxexamples/react/start-appwrite-basic/src/hooks/useMutation.tsexamples/react/start-appwrite-basic/src/routeTree.gen.tsexamples/react/start-appwrite-basic/src/router.tsxexamples/react/start-appwrite-basic/src/routes/__root.tsxexamples/react/start-appwrite-basic/src/routes/_authed.tsxexamples/react/start-appwrite-basic/src/routes/_authed/posts.$postId.tsxexamples/react/start-appwrite-basic/src/routes/_authed/posts.index.tsxexamples/react/start-appwrite-basic/src/routes/_authed/posts.tsxexamples/react/start-appwrite-basic/src/routes/index.tsxexamples/react/start-appwrite-basic/src/routes/login.tsxexamples/react/start-appwrite-basic/src/routes/logout.tsxexamples/react/start-appwrite-basic/src/routes/signup.tsxexamples/react/start-appwrite-basic/src/styles/app.cssexamples/react/start-appwrite-basic/src/utils/appwrite.tsexamples/react/start-appwrite-basic/src/utils/posts.tsexamples/react/start-appwrite-basic/src/utils/seo.tsexamples/react/start-appwrite-basic/tsconfig.jsonexamples/react/start-appwrite-basic/vite.config.ts
| /public/build# Sentry Config File | ||
| .env.sentry-build-plugin |
There was a problem hiding this comment.
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.
| /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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
| <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" |
There was a problem hiding this comment.
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.
| <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"> | |
| </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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
| 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), | ||
| }) |
There was a problem hiding this comment.
🧩 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:
- 1: https://tanstack.com/start/latest/docs/framework/react/guide/server-functions
- 2: https://tanstack.com/start/v0/docs/framework/react/guide/server-functions
- 3: https://stackoverflow.com/questions/79780632/what-is-the-use-case-for-tanstack-start-useserverfn-hook
- 4: https://brenelz.com/posts/using-server-functions-and-tanstack-query/
🏁 Script executed:
cat -n examples/react/start-appwrite-basic/src/components/Login.tsx | head -40Repository: TanStack/router
Length of output: 1354
🏁 Script executed:
find examples/react/start-appwrite-basic/src/routes -name '*authed*' -o -name 'signup*' | head -10Repository: TanStack/router
Length of output: 230
🏁 Script executed:
cat -n examples/react/start-appwrite-basic/src/routes/_authed.tsxRepository: TanStack/router
Length of output: 1395
🏁 Script executed:
cat -n examples/react/start-appwrite-basic/src/routes/signup.tsxRepository: 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.
| 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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`.
| .using-mouse * { | ||
| outline: none !important; | ||
| } |
There was a problem hiding this comment.
Preserve keyboard-visible focus styles.
Line 28 disables every outline under .using-mouse, which can hide focus indication if that class is still present during keyboard navigation. Scope this to non-keyboard focus states instead.
Suggested CSS adjustment
- .using-mouse * {
- outline: none !important;
+ .using-mouse *:focus:not(:focus-visible) {
+ outline: none;
}📝 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.
| .using-mouse * { | |
| outline: none !important; | |
| } | |
| .using-mouse *:focus:not(:focus-visible) { | |
| outline: none; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/react/start-appwrite-basic/src/styles/app.css` around lines 27 - 29,
The rule ".using-mouse * { outline: none !important; }" removes all focus
outlines even when keyboard users are navigating; change it to only remove
outline for focus states that are not keyboard-visible (e.g., target
".using-mouse *:focus:not(:focus-visible)" or the equivalent
:focus:not(:focus-visible) selector) so that keyboard-visible focus indicators
remain visible while mouse-only focus still hides outlines.
There was a problem hiding this comment.
Inherited verbatim from examples/react/start-supabase-basic/src/styles/app.css. Keeping parity with the existing example; happy to do the :focus:not(:focus-visible) upgrade in a follow-up PR that covers both.
There was a problem hiding this comment.
@ChiragAgg5k, that makes sense — keeping the two examples structurally identical is a good approach, and a single follow-up PR that upgrades both is the right way to handle it. I'll withdraw the concern for now.
Would you like me to open a GitHub issue to track the :focus:not(:focus-visible) upgrade for both start-appwrite-basic and start-supabase-basic?
✏️ Learnings added
Learnt from: ChiragAgg5k
Repo: TanStack/router PR: 7225
File: examples/react/start-appwrite-basic/src/styles/app.css:27-29
Timestamp: 2026-04-19T10:49:38.517Z
Learning: In `examples/react/start-appwrite-basic/src/styles/app.css` (and its counterpart `examples/react/start-supabase-basic/src/styles/app.css`), the `.using-mouse * { outline: none !important; }` rule is intentionally shared verbatim between the two example projects for parity. Any accessibility improvements (e.g., scoping to `:focus:not(:focus-visible)`) should be applied to both files in the same 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.
🧠 Learnings used
Learnt from: hokkyss
Repo: TanStack/router PR: 5418
File: e2e/react-start/custom-identifier-prefix/src/styles/app.css:19-21
Timestamp: 2025-10-09T12:59:02.129Z
Learning: In e2e test directories (paths containing `e2e/`), accessibility concerns like outline suppression patterns are less critical since the code is for testing purposes, not production use.
| export type PostType = { | ||
| id: string | ||
| title: string | ||
| body: string | ||
| } | ||
|
|
||
| export const fetchPost = createServerFn({ method: 'GET' }) | ||
| .inputValidator((d: string) => d) | ||
| .handler(async ({ data: postId }) => { | ||
| console.info(`Fetching post with id ${postId}...`) | ||
| const post = await axios | ||
| .get<PostType>(`https://jsonplaceholder.typicode.com/posts/${postId}`) | ||
| .then((r) => r.data) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Confirm JSONPlaceholder returns numeric post IDs.
# Expected: id_type is "number".
curl -s https://jsonplaceholder.typicode.com/posts/1 | jq '{id, id_type: (.id | type)}'Repository: TanStack/router
Length of output: 95
🏁 Script executed:
cat -n examples/react/start-appwrite-basic/src/utils/posts.tsRepository: TanStack/router
Length of output: 1331
Normalize JSONPlaceholder IDs before returning PostType.
Lines 16 and 34 assert the JSONPlaceholder response directly matches PostType, but the API returns numeric IDs while PostType.id is declared as string. This creates a runtime/type mismatch. Normalize the external payload instead of relying on the type assertion. Per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.
Proposed fix
export type PostType = {
id: string
title: string
body: string
}
+
+type JsonPlaceholderPost = Omit<PostType, 'id'> & {
+ id: number
+}
+
+function normalizePost(post: JsonPlaceholderPost): PostType {
+ return {
+ ...post,
+ id: String(post.id),
+ }
+}
export const fetchPost = createServerFn({ method: 'GET' })
.inputValidator((d: string) => d)
.handler(async ({ data: postId }) => {
console.info(`Fetching post with id ${postId}...`)
const post = await axios
- .get<PostType>(`https://jsonplaceholder.typicode.com/posts/${postId}`)
- .then((r) => r.data)
+ .get<JsonPlaceholderPost>(
+ `https://jsonplaceholder.typicode.com/posts/${postId}`,
+ )
+ .then((r) => normalizePost(r.data))
.catch((err) => {
console.error(err)
if (err.status === 404) {
@@
return axios
- .get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts')
- .then((r) => r.data.slice(0, 10))
+ .get<Array<JsonPlaceholderPost>>(
+ 'https://jsonplaceholder.typicode.com/posts',
+ )
+ .then((r) => r.data.slice(0, 10).map(normalizePost))
},
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/react/start-appwrite-basic/src/utils/posts.ts` around lines 5 - 17,
The handler in fetchPost currently assumes the external JSONPlaceholder payload
matches PostType (id:string) but the API returns id as a number; modify the
fetch to parse/normalize the response before returning: call axios.get with an
appropriate external response shape (e.g., id:number, title:string, body:string)
or use any, then map the received object to PostType by converting id to a
string (e.g., id: String(response.id)) and copying title/body; remove the
incorrect generic <PostType> on axios.get and ensure the handler returns the
normalized PostType object.
There was a problem hiding this comment.
Inherited verbatim from examples/react/start-supabase-basic/src/utils/posts.ts. The {id: String} template-literal interpolation of post.id still works at runtime in the <Link params> path and there is no current consumer that relies on strict equality against the typed string, so this has not surfaced as a real bug. Keeping parity with the existing example; happy to normalize both in a follow-up PR.
There was a problem hiding this comment.
@ChiragAgg5k, that's a fair point — maintaining parity with start-supabase-basic is a reasonable constraint here, and JavaScript's implicit coercion in template literals does make the numeric-vs-string mismatch benign at runtime. I'll note this for future reviews of similar example files.
Would you like me to open a follow-up GitHub issue to track normalizing PostType.id in both start-appwrite-basic and start-supabase-basic together?
(´• ω •`) 🐇
🧠 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`.
Learnt from: nlynzaad
Repo: TanStack/router PR: 6389
File: e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx:11-24
Timestamp: 2026-01-18T17:36:24.388Z
Learning: In TanStack Router e2e malformed test routes (e.g., `e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx`), intentional use of `href` prop instead of `to` prop on Link components may be used for testing purposes to validate specific encoding/decoding behaviors or edge cases, even though the standard `tanstack/solid-router` Link API uses the `to` prop.
| const tags = [ | ||
| { title }, | ||
| { name: 'description', content: description }, | ||
| { name: 'keywords', content: keywords }, | ||
| { name: 'twitter:title', content: title }, | ||
| { name: 'twitter:description', content: description }, | ||
| { name: 'twitter:creator', content: '@tannerlinsley' }, | ||
| { name: 'twitter:site', content: '@tannerlinsley' }, | ||
| { name: 'og:type', content: 'website' }, | ||
| { name: 'og:title', content: title }, | ||
| { name: 'og:description', content: description }, | ||
| ...(image | ||
| ? [ | ||
| { name: 'twitter:image', content: image }, | ||
| { name: 'twitter:card', content: 'summary_large_image' }, | ||
| { name: 'og:image', content: image }, | ||
| ] | ||
| : []), | ||
| ] |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Open Graph protocol meta tags property attribute og:title og:image
💡 Result:
The Open Graph (OG) protocol uses meta tags with the attribute name="property" and values prefixed with "og:", such as property="og:title" and property="og:image". These are placed in the section of HTML pages to control how content appears when shared on social media platforms like Facebook, LinkedIn, and others. og:title: Specifies the title of the page or object as it should appear in the social graph preview. Example: . It should be concise, ideally under 60-75 characters. og:image: Specifies the URL of an image to represent the object in the preview. Recommended size is 1200x630 pixels (1.91:1 aspect ratio) for optimal display. Supports structured properties like og:image:width, og:image:height, og:image:alt, og:image:type (e.g., image/jpeg), and og:image:secure_url for HTTPS. Example: . These are part of the four required basic OG tags (along with og:type and og:url), as defined in the official specification. Facebook's documentation confirms the property attribute usage and provides additional guidance on images.
Citations:
- 1: https://developers.facebook.com/docs/sharing/webmasters/
- 2: https://opengraphprotocol.org/
- 3: https://ogp.me/
- 4: https://insights.theinteractive.studio/what-are-open-graph-metatags-and-how-to-use-them-in-web-pages
- 5: https://opengraphpro.com/en-GB/docs/opengraph-tags
🏁 Script executed:
cat -n examples/react/start-appwrite-basic/src/utils/seo.tsRepository: TanStack/router
Length of output: 1174
🏁 Script executed:
cd examples/react/start-appwrite-basic && grep -r "seo(" --include="*.ts" --include="*.tsx" | head -20Repository: TanStack/router
Length of output: 95
🏁 Script executed:
cd examples/react/start-appwrite-basic && grep -r "from.*seo" --include="*.ts" --include="*.tsx"Repository: TanStack/router
Length of output: 115
🏁 Script executed:
sed -n '1,100p' examples/react/start-appwrite-basic/src/routes/__root.tsxRepository: TanStack/router
Length of output: 2384
🏁 Script executed:
cd examples/react/start-appwrite-basic && find . -name "*.md" -o -name "*.json" | grep -i "tanstack\|router" | head -10Repository: TanStack/router
Length of output: 41
🏁 Script executed:
grep -r "HeadContent\|head:" examples/react/start-appwrite-basic/src --include="*.ts" --include="*.tsx" -A 5 | head -40Repository: TanStack/router
Length of output: 1418
Use property for Open Graph tags and conditionally emit optional meta fields.
Open Graph meta tags (lines 20-22, 27) must use property="og:*" instead of name="og:*" per the Open Graph protocol specification. Additionally, optional fields (description, keywords) should be conditionally emitted to avoid creating meta tags with content: undefined.
Suggested metadata cleanup
const tags = [
{ title },
- { name: 'description', content: description },
- { name: 'keywords', content: keywords },
+ ...(description ? [{ name: 'description', content: description }] : []),
+ ...(keywords ? [{ name: 'keywords', content: keywords }] : []),
{ name: 'twitter:title', content: title },
- { name: 'twitter:description', content: description },
+ ...(description
+ ? [{ name: 'twitter:description', content: description }]
+ : []),
{ name: 'twitter:creator', content: '@tannerlinsley' },
{ name: 'twitter:site', content: '@tannerlinsley' },
- { name: 'og:type', content: 'website' },
- { name: 'og:title', content: title },
- { name: 'og:description', content: description },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:title', content: title },
+ ...(description
+ ? [{ property: 'og:description', content: description }]
+ : []),
...(image
? [
{ name: 'twitter:image', content: image },
{ name: 'twitter:card', content: 'summary_large_image' },
- { name: 'og:image', content: image },
+ { property: 'og:image', content: image },
]
: []),
]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/react/start-appwrite-basic/src/utils/seo.ts` around lines 12 - 30,
The tags array in seo.ts currently uses name="og:*" and always emits
description/keywords which can be undefined; update the construction of the tags
array (the tags constant that references title, description, keywords, image)
to: 1) use property: 'og:*' for Open Graph entries (e.g., property: 'og:title',
property: 'og:description', property: 'og:type', property: 'og:image') instead
of name, and 2) conditionally push meta objects only when their values exist
(guard emitting description, keywords, twitter:* and og:image when
description/keywords/image are truthy) so no meta tag has content: undefined.
Ensure references remain to the same variables (title, description, keywords,
image) and preserve existing twitter card logic when image is present.
There was a problem hiding this comment.
Inherited verbatim from examples/react/start-supabase-basic/src/utils/seo.ts. Keeping parity with the existing example; happy to do the property: 'og:*' + conditional-emit upgrade in a follow-up PR that covers both.
There was a problem hiding this comment.
@ChiragAgg5k, that makes perfect sense — keeping the Appwrite and Supabase examples in lockstep is a good call. Fixing both in one follow-up PR is cleaner than having them diverge. Would you like me to open a tracking GitHub issue for the property: 'og:*' + conditional-emit cleanup so it doesn't fall through the cracks?
✏️ Learnings added
Learnt from: ChiragAgg5k
Repo: TanStack/router PR: 7225
File: examples/react/start-appwrite-basic/src/utils/seo.ts:12-30
Timestamp: 2026-04-19T10:49:42.382Z
Learning: In the TanStack Router repository, `examples/react/start-appwrite-basic/src/utils/seo.ts` is intentionally copied verbatim from `examples/react/start-supabase-basic/src/utils/seo.ts` to maintain parity between the two examples. Any fixes (e.g., using `property: 'og:*'` for Open Graph tags, conditionally emitting optional meta fields) should be applied to both files together in the same PR rather than fixing one in isolation.
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.
- __root.tsx: drop invalid 5-digit hex `#fffff` and the misplaced `color` attr from the manifest link; add `lang="en"` to the root `<html>` for screen-reader / a11y audits - signup.tsx: reorder imports to satisfy ESLint `sort-imports` - signup.tsx: on `createEmailPasswordSession` failure after a successful `account.create`, roll back the newly created user via the admin Users service so retries don't hit `user_already_exists` (Appwrite-specific — the two-call pattern is only in this example, not in start-supabase-basic) - appwrite.ts: add `createAdminUsers()` helper so the signup handler can perform that rollback with the same credentials as `createAdminClient`
What does it do
Adds a new React Start example,
examples/react/start-appwrite-basic, that demonstrates end-to-end authentication with Appwrite — the same feature surface asstart-supabase-basic(sign up, log in, log out, a rootfetchUser, and a_authedlayout that gates/posts).Shape mirrors
start-supabase-basicso the two examples are directly comparable; only the auth utility and the server functions that call it differ.What's inside
src/utils/appwrite.ts— Appwrite SSR helpers built onnode-appwrite:createSessionClient()reads the session secret from theappwrite-sessioncookie (via@tanstack/react-start/server'sgetCookie) and returns anAccountclient scoped to that session.createAdminClient()uses the serverAPPWRITE_API_KEYfor the sign-up / sign-in handshakes that need to issue new sessions.setAppwriteSessionCookie/clearAppwriteSessionCookiecookie helpers.src/routes/_authed.tsx—loginFnserver function →account.createEmailPasswordSession+ cookie;beforeLoadgate + Login error component.src/routes/signup.tsx—signupFn→account.create+ session + cookie + redirect.src/routes/logout.tsx— session delete + cookie clear + redirect.src/routes/__root.tsx—fetchUservia the session client'saccount.get().docs/start/config.json— adds "Basic + Appwrite Auth" under React examples.docs/start/framework/react/guide/authentication-overview.md— lists the new example alongside the Supabase one under DIY Implementations.The mock posts data layer (
jsonplaceholderviaredaxios) is unchanged, matching the Supabase example.How to run
An Appwrite project with Email/Password auth enabled and a server API Key that has
sessions.writeandusers.writescopes.Testing
Verified against a real Appwrite Cloud project end-to-end:
node-appwrite): adminaccount.create→createEmailPasswordSession→account.getwith session client → session delete → admin user delete. All green.GET /without cookie → SSR payload containsuser:null.GET /with a validappwrite-session=<secret>cookie → SSR payload renders the user's email.GET /postswith cookie → 200, nav shows "Logout".GET /postswithout cookie →_authed.errorComponentrenders the Login form./postsrenders → logout →/postsfalls back to Login → bad-creds shows the inline error and the "Sign up instead?" fallback → login with correct creds lands back on Home.All screenshots below are captured against the running dev server.
Signed-out Home
Sign-up form (empty → filled)
After sign up — auto-logged in, nav shows user email + Logout
/postswith session (the_authedlayout passes through)/postsafter logout —_authed.errorComponentrenders the Login formInvalid credentials — Appwrite error surfaced inline, "Sign up instead?" fallback
Valid login redirects back to Home
Local checks
pnpm nx run-many --target=build --projects=@tanstack/react-start,@tanstack/react-router,…— clean.tsc --noEmitin the example — clean.vite build && tsc --noEmit(the example'sbuildscript) — clean.pnpm exec eslint examples/react/start-appwrite-basic— 5 errors, all pre-existing in the Supabase template (sort-importsoncreateFileRoute,newline-after-importin two route files, and oneno-unnecessary-condition— identical baseline).Happy to adjust the folder name, README wording, or add a Solid mirror (
examples/solid/start-appwrite-basic) if that would be preferred in a follow-up.Summary by CodeRabbit
New Features
Documentation
Chores