diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 3190b68..be845bf 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -7,14 +7,17 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ envDir: path.resolve(import.meta.dirname, "../.."), plugins: [tailwindcss(), reactRouter()], - ssr: { - noExternal: true, - }, resolve: { dedupe: ["convex", "react", "react-dom"], tsconfigPaths: true, }, server: { allowedHosts: true, + proxy: { + "/api/auth": { + changeOrigin: true, + target: "https://befitting-dalmatian-161.convex.site", + }, + }, }, }); diff --git a/docs/auth-proxy.md b/docs/auth-proxy.md new file mode 100644 index 0000000..c8c5edb --- /dev/null +++ b/docs/auth-proxy.md @@ -0,0 +1,78 @@ +# Auth Proxy — Production Ingress Requirement + +The application uses **same-origin authentication**: the browser and React Router SSR both hit +`/api/auth/*` on the public application domain. This keeps cookies first-party, avoids +cross-origin credentials, and gives development and production the same API surface. + +## Required production route + +At the public application domain, route: + +| Prefix | Target | +|---|---| +| `/api/auth/*` | Convex HTTP site | +| `/*` | React Router frontend | + +### Caddy + +```caddy +zopu.example.com { + handle /api/auth/* { + reverse_proxy https://befitting-dalmatian-161.convex.site { + header_up Host befitting-dalmatian-161.convex.site + } + } + + handle { + reverse_proxy frontend:3000 + } +} +``` + +### Dokploy / Traefik + +Create a higher-priority path router for `/api/auth` that forwards to the external Convex +site URL. Ensure it: + +- Preserves the original browser `Cookie` header +- Passes `Set-Cookie` responses back (rewrite domain if Convex emits an explicit one) +- Preserves the original method and body +- Forwards `X-Forwarded-Host` and `X-Forwarded-Proto` +- Passes the full path unchanged (e.g. `/api/auth/convex/token`) +- Does not cache auth responses +- Allows OAuth callback routes under the same prefix + +## Convex environment + +The Convex deployment `SITE_URL` must match the public origin users visit, not the Convex +site URL: + +```bash +# Production +npx convex env set SITE_URL 'https://zopu.example.com' + +# Local +npx convex env set SITE_URL 'http://100.101.157.28:5173' + +# Staging +npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf' +``` + +This value populates Better Auth's `trustedOrigins`. The Convex deployment also uses +`CONVEX_SITE_URL` as the internal `baseURL` for route registration; that value is the HTTP +site URL (e.g. `https://befitting-dalmatian-161.convex.site`). + +## Why same-origin + +1. **First-party cookies.** No `SameSite=None`, no third-party cookie restrictions. +2. **SSR consistency.** The React Router server uses the same auth surface the browser + sees; no cross-host credential translation. +3. **No CORS complexity.** The browser talks only to its own origin. +4. **The reverse proxy handles the cross-host hop server-side.** + +## Related + +- [Better Auth: Trusted Origins](https://github.com/better-auth/better-auth/blob/main/docs/content/docs/reference/security.mdx) +- `apps/web/vite.config.ts` — Vite dev proxy (`/api/auth` → Convex site) +- `packages/auth/src/web/auth-client.ts` — `window.location.origin` +- `apps/web/src/lib/auth.server.ts` — SSR token loader via request origin diff --git a/packages/backend/convex/auth.ts b/packages/backend/convex/auth.ts index bf9ae7f..391b9e8 100644 --- a/packages/backend/convex/auth.ts +++ b/packages/backend/convex/auth.ts @@ -17,6 +17,7 @@ export const authComponent = createClient(components.betterAuth); const createAuth = (ctx: GenericCtx) => betterAuth({ + advanced: { useSecureCookies: siteUrl.startsWith("https://") }, baseURL: env.CONVEX_SITE_URL, database: authComponent.adapter(ctx), emailAndPassword: {