diff --git a/.env.production.example b/.env.production.example
index 01a1951..48113b9 100644
--- a/.env.production.example
+++ b/.env.production.example
@@ -1,8 +1,25 @@
OPENROUTER_API_KEY=
-AUTH_AUTHENTIK_CLIENT_ID=
-AUTH_AUTHENTIK_CLIENT_SECRET=XXXXXXXXXXXXXXXX
-AUTH_AUTHENTIK_ISSUER=XXXXXXXXXXXXXXXXXXX
-BETTER_AUTH_URL=XXXXXXXXXXXXXXXXXXX
-BETTER_AUTH_SECRET=XXXXXXXXXXX
-DB_URL=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+# --- Auth (all providers are optional; configure only what you need) ---
+
+# Authentik (genericOAuth)
+AUTH_AUTHENTIK_CLIENT_ID=
+AUTH_AUTHENTIK_CLIENT_SECRET=
+AUTH_AUTHENTIK_ISSUER=
+
+# Google OAuth — https://console.cloud.google.com/
+AUTH_GOOGLE_CLIENT_ID=
+AUTH_GOOGLE_CLIENT_SECRET=
+
+# Apple Sign In — https://developer.apple.com/account/resources/authkeys/list
+# AUTH_APPLE_CLIENT_SECRET is a pre-generated JWT signed with the .p8 key.
+# See: https://better-auth.com/docs/authentication/apple
+AUTH_APPLE_CLIENT_ID=
+AUTH_APPLE_CLIENT_SECRET=
+AUTH_APPLE_TEAM_ID=
+AUTH_APPLE_KEY_ID=
+AUTH_APPLE_PRIVATE_KEY=
+
+BETTER_AUTH_URL=
+BETTER_AUTH_SECRET=
+DATABASE_URL=
diff --git a/APPLE-OAUTH.md b/APPLE-OAUTH.md
new file mode 100644
index 0000000..ef0394d
--- /dev/null
+++ b/APPLE-OAUTH.md
@@ -0,0 +1,195 @@
+# Apple OAuth Setup
+
+Reference for when the Apple Developer account is ready.
+No code changes needed — just fill in the five env vars and restart.
+
+## What you need
+
+- An **Apple Developer Program** membership ($99/yr) — [enroll here](https://developer.apple.com/programs/enroll/)
+- The app must be served over **HTTPS** in production (localhost works for dev — see note in Step 2)
+
+## The five env vars
+
+```bash
+AUTH_APPLE_CLIENT_ID= # Services ID identifier (Step 2)
+AUTH_APPLE_CLIENT_SECRET= # Pre-generated JWT (Step 4)
+AUTH_APPLE_TEAM_ID= # 10-char team identifier (Step 1)
+AUTH_APPLE_KEY_ID= # 10-char key identifier (Step 3)
+AUTH_APPLE_PRIVATE_KEY= # Contents of the .p8 file (Step 3)
+```
+
+All five must be non-empty or the Apple button won't appear.
+Set any subset to empty to disable the provider without touching code.
+
+---
+
+## Step 1 — Note your Team ID
+
+Sign in to [developer.apple.com](https://developer.apple.com/account).
+Your **Team ID** is the 10-character string shown in the top-right corner
+next to your name (e.g. `ABCD123456`).
+
+```
+AUTH_APPLE_TEAM_ID=ABCD123456
+```
+
+---
+
+## Step 2 — Create an App ID
+
+1. [Certificates, IDs & Profiles → Identifiers](https://developer.apple.com/account/resources/identifiers/list)
+2. Click **+** → **App IDs** → **App** → Continue
+3. **Description**: `Local Cal`
+4. **Bundle ID** (Explicit): `com.yourdomain.localcal`
+5. Under **Capabilities** scroll to **Sign In with Apple** → check it →
+ **Configure** → **Enable as primary App ID** → Done
+6. Continue → **Register**
+
+---
+
+## Step 3 — Create a Services ID (the OAuth client ID)
+
+This is what identifies the *web* sign-in flow — separate from the App ID above.
+
+1. **Identifiers → +** → **Services IDs** → Continue
+2. **Description**: `Local Cal Web`
+3. **Identifier**: `com.yourdomain.localcal.web`
+ *(must differ from the Bundle ID)*
+4. Continue → **Register**
+5. Click the Services ID you just created
+6. Enable **Sign In with Apple** → **Configure**
+ - **Primary App ID**: select `com.yourdomain.localcal` (Step 2)
+ - **Domains and Subdomains**: `yourdomain.com`
+ *(no `https://`, no trailing slash, no path)*
+ - **Return URLs**:
+ ```
+ https://yourdomain.com/api/auth/callback/apple
+ ```
+ > **localhost dev**: add `localhost` as a domain and
+ > `http://localhost:3000/api/auth/callback/apple` as a return URL.
+ > Apple permits plain HTTP for localhost only.
+ > Alternatively use an HTTPS tunnel:
+ > [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).
+7. Done → Continue → **Save**
+
+```
+AUTH_APPLE_CLIENT_ID=com.yourdomain.localcal.web
+```
+
+---
+
+## Step 4 — Create a private key
+
+1. [Keys → +](https://developer.apple.com/account/resources/authkeys/list)
+2. **Key Name**: `Local Cal Sign In`
+3. Check **Sign In with Apple** → **Configure** → select the App ID from Step 2 → Save
+4. Continue → **Register**
+5. **Download the `.p8` file immediately** — Apple only lets you download it once.
+ Store it somewhere safe (password manager, secrets vault).
+ The filename will be `AuthKey_XXXXXXXXXX.p8`.
+
+The **Key ID** (`AUTH_APPLE_KEY_ID`) is shown on the key detail page.
+
+```
+AUTH_APPLE_KEY_ID=ABC1234567
+```
+
+Paste the full contents of the `.p8` file into `AUTH_APPLE_PRIVATE_KEY`:
+
+```bash
+# .env.local — wrap multi-line value in double quotes
+AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg...
+-----END PRIVATE KEY-----"
+```
+
+On deployment platforms (Vercel, Railway, Fly.io) paste the value as-is —
+they handle multi-line secrets natively without quotes.
+
+---
+
+## Step 5 — Generate the client secret JWT
+
+Apple does not use a static client secret. Instead you sign a short-lived JWT
+with the private key. It is valid for up to **180 days**.
+
+Run this once with Bun:
+
+```ts
+// scripts/gen-apple-secret.ts
+import { importPKCS8, SignJWT } from "jose";
+import { readFileSync } from "fs";
+
+const TEAM_ID = "ABCD123456"; // Step 1
+const CLIENT_ID = "com.yourdomain.localcal.web"; // Step 3
+const KEY_ID = "ABC1234567"; // Step 4
+const PRIVATE_KEY = readFileSync("./AuthKey_ABC1234567.p8", "utf8");
+
+const key = await importPKCS8(PRIVATE_KEY, "ES256");
+const now = Math.floor(Date.now() / 1000);
+
+const jwt = await new SignJWT({})
+ .setProtectedHeader({ alg: "ES256", kid: KEY_ID })
+ .setIssuer(TEAM_ID)
+ .setSubject(CLIENT_ID)
+ .setAudience("https://appleid.apple.com")
+ .setIssuedAt(now)
+ .setExpirationTime(now + 180 * 24 * 60 * 60)
+ .sign(key);
+
+console.log(jwt);
+```
+
+```bash
+bun run scripts/gen-apple-secret.ts
+```
+
+Copy the printed JWT:
+
+```
+AUTH_APPLE_CLIENT_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6Ii...
+```
+
+> **Set a reminder** to regenerate ~5 months from now.
+> When the JWT expires the Apple button will silently fail at sign-in.
+
+---
+
+## Completed `.env.local` block
+
+```bash
+AUTH_APPLE_CLIENT_ID=com.yourdomain.localcal.web
+AUTH_APPLE_CLIENT_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6Ii...
+AUTH_APPLE_TEAM_ID=ABCD123456
+AUTH_APPLE_KEY_ID=ABC1234567
+AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg...
+-----END PRIVATE KEY-----"
+```
+
+Restart the dev server — the Apple button appears automatically.
+
+---
+
+## Verifying without a device
+
+```bash
+bun test tests/auth.test.ts
+```
+
+The tests mock env vars and confirm the Apple provider registers correctly.
+A passing suite means the wiring is correct before you touch the portal.
+
+---
+
+## Troubleshooting
+
+| Symptom | Cause | Fix |
+|---|---|---|
+| Apple button missing | Any of the 5 vars is empty | All five must be non-empty |
+| `invalid_client` | JWT expired or signed with wrong key | Regenerate `AUTH_APPLE_CLIENT_SECRET` (Step 5) |
+| `invalid_grant` | `CLIENT_ID` is the Bundle ID, not the Services ID | Use `com.yourdomain.localcal.web` (Step 3) |
+| Redirect fails in dev | Localhost not registered as a domain | Add `localhost` + `http://localhost:3000/api/auth/callback/apple` in Step 3 |
+| Redirect fails in prod | Domain/return URL not registered | Re-check the Services ID configuration in Step 3 |
+| Name is blank after sign-in | Apple only sends the user's name on the **first** sign-in | Expected — store the name on that first callback |
+| Email is a relay address | User chose "Hide My Email" | Expected — treat it as canonical, it routes to their real inbox |
diff --git a/compose.dev.yml b/compose.dev.yml
deleted file mode 100644
index ea119c9..0000000
--- a/compose.dev.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-services:
- postgres:
- image: postgres:17
- container_name: local-cal-postgres
- ports:
- - "5432:5432"
- environment:
- POSTGRES_USER: localcal
- POSTGRES_PASSWORD: localcal
- POSTGRES_DB: localcal
- volumes:
- - postgres_data:/var/lib/postgresql/data
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U localcal"]
- interval: 5s
- timeout: 5s
- retries: 5
-
-volumes:
- postgres_data:
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx
index 749b78e..8154ee4 100644
--- a/src/app/auth/signin/page.tsx
+++ b/src/app/auth/signin/page.tsx
@@ -1,74 +1,10 @@
-"use client";
-
-import { motion } from "framer-motion";
-import { CalendarDays, Loader2 } from "lucide-react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { signIn, useSession } from "@/lib/auth-client";
+// Server Component — reads env vars and passes provider list to the client UI
+import { getSignInProviders } from "@/lib/get-sign-in-providers";
+import { SignInForm } from "./sign-in-form";
export default function SignInPage() {
- const { data: session, isPending } = useSession();
- const router = useRouter();
- const [isLoading, setIsLoading] = useState(false);
-
- useEffect(() => {
- if (session?.user) {
- router.push("/");
- }
- }, [session, router]);
-
- const handleSignIn = async () => {
- setIsLoading(true);
- try {
- await signIn.oauth2({
- providerId: "authentik",
- callbackURL: "/",
- });
- } catch {
- toast.error("Failed to sign in. Please try again.");
- } finally {
- setIsLoading(false);
- }
- };
-
- if (isPending || session?.user) {
- return null;
- }
-
- return (
-