Learn how to set up your first TanStack Start project with the Appwrite React library. The library exposes a TanStack file-route handler, server helpers, and the same React hooks you use on the client.
Head to the Appwrite Console.
If this is your first time using Appwrite, create an account and create your first project.
Then, under Add a platform, add a Web app. The Hostname should be localhost.
Cross-Origin Resource Sharing (CORS)
Adding localhost as a platform lets your local app talk to Appwrite. For production, add your live domain to avoid CORS errors.
Learn more in our CORS error guide.
You can skip optional steps.
In your project, go to Overview > Integrations > API keys and create a new key with the scopes users.read, users.write, and sessions.write. Copy the key secret. The SSR handler uses this to create sessions on behalf of users; never expose it to the browser.
Create a TanStack Start project.
npx @tanstack/cli create my-app --framework react && cd my-app
Install the React library along with the Appwrite Web SDK, Appwrite Node SDK, and @tanstack/react-query packages.
npm install @appwrite.io/react appwrite node-appwrite @tanstack/react-query
Create a .env file at the project root. Replace <REGION>, <PROJECT_ID>, and <API_KEY> with your own values.
VITE_APPWRITE_ENDPOINT=https://<REGION>.cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=<PROJECT_ID>
APPWRITE_API_KEY=<API_KEY>
VITE_* values are shipped to the browser. APPWRITE_API_KEY stays server-only.
Create src/routes/api/appwrite/$.ts. The TanStack file route exposes the library's sign-in, sign-up, sign-out, and oauth/callback endpoints under /api/appwrite/*.
import { createFileRoute } from "@tanstack/react-router";
import { createAppwriteHandlers } from "@appwrite.io/react/handlers/tanstack";
export const Route = createFileRoute("/api/appwrite/$")({
server: {
handlers: createAppwriteHandlers({
endpoint: import.meta.env.VITE_APPWRITE_ENDPOINT,
projectId: import.meta.env.VITE_APPWRITE_PROJECT_ID,
apiKey: process.env.APPWRITE_API_KEY!,
basePath: "/api/appwrite",
}),
},
});
Replace src/routes/index.tsx with the following. A createServerFn loader reads the session cookie server-side; the page receives the result via Route.useLoaderData() and passes it into AppwriteProvider.
import { useState } from "react";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { AppwriteProvider, useAuth } from "@appwrite.io/react";
import { createTanStackServerHelpers } from "@appwrite.io/react/server/tanstack";
const getAuthSnapshot = createServerFn({ method: "GET" }).handler(async () => {
const helpers = createTanStackServerHelpers({
endpoint: import.meta.env.VITE_APPWRITE_ENDPOINT,
projectId: import.meta.env.VITE_APPWRITE_PROJECT_ID,
});
return {
session: helpers.readSessionCookie() ?? null,
user: await helpers.getLoggedInUser(),
};
});
export const Route = createFileRoute("/")({
loader: () => getAuthSnapshot(),
component: Page,
});
function Page() {
const { session, user } = Route.useLoaderData();
return (
<AppwriteProvider
endpoint={import.meta.env.VITE_APPWRITE_ENDPOINT}
projectId={import.meta.env.VITE_APPWRITE_PROJECT_ID}
ssr={{ session, basePath: "/api/appwrite" }}
>
<main>
<h1>Appwrite React library on TanStack Start</h1>
<p>SSR user: {user?.email ?? "signed out"}</p>
<AuthPanel />
</main>
</AppwriteProvider>
);
}
function AuthPanel() {
const { user, isLoading, signIn, signUp, signOut, error } = useAuth();
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
if (isLoading) return <p>Loading...</p>;
if (user) {
return (
<div>
<p>Welcome, {user.name || user.email}</p>
<button onClick={() => signOut.signOut({ onSuccess: () => router.invalidate() })}>
Sign out
</button>
</div>
);
}
return (
<div>
<input placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
<input placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
<input
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={() =>
signUp.emailPassword({
email,
password,
name,
onSuccess: () => router.invalidate(),
})
}
disabled={signUp.isPending}
>
Sign up
</button>
<button
onClick={() =>
signIn.emailPassword({
email,
password,
onSuccess: () => router.invalidate(),
})
}
disabled={signIn.isPending}
>
Sign in
</button>
{error && <p style={{ color: "red" }}>{error.message}</p>}
</div>
);
}
After every auth mutation, router.invalidate() re-runs the loader so the SSR user reflects the new session cookie.
Multi-route apps
In a multi-route TanStack Start app, move AppwriteProvider (and the auth loader) into src/routes/__root.tsx so the TanStack Query cache and auth state persist across navigations. Mounting the provider in a single route works for this quickstart, but resets the cache every time the route changes.
npm run dev
Open localhost on port 3000. Sign up, sign out, and sign back in to verify the cookie-based SSR flow.
Next steps
For server-side admin operations, per-request session clients, OAuth callbacks, and the full hook reference, see the React library docs.