···11# Database
22DATABASE_URL="postgresql://user:password@localhost:5432/askimut"
3344-# OAuth
55-PUBLIC_URL="http://localhost:3000"
44+# OAuth — local dev
55+# The app must be served on a different loopback host than the PDS so the
66+# browser reports Sec-Fetch-Site: cross-site on /oauth/authorize (the atproto
77+# oauth-provider rejects same-site authorize requests). We use IPv6 loopback
88+# [::1] for the app and IPv4 loopback (localhost) for the PDS.
99+APP_URL=http://[::1]:3000
1010+HOST=::
1111+PORT=3000
1212+NITRO_HOST=::
1313+NITRO_PORT=3000
1414+1515+# In production, set PUBLIC_URL to your real HTTPS origin instead of APP_URL:
1616+# PUBLIC_URL="https://askimut.example.com"
617718# AT Protocol Configuration
819# Enable/disable AT Protocol publishing (default: false for development)
···2031AT_PROTOCOL_PDS=https://bsky.social
2132AT_PROTOCOL_APPVIEW=https://api.bsky.app
22332323-# Local dev network (run `pnpm run dev:network` to start)
2424-# Ports are assigned dynamically — copy the printed values here.
2525-# DEV_PDS_URL=http://127.0.0.1:XXXX
2626-# DEV_PLC_URL=http://127.0.0.1:XXXX
3434+# Local dev network (run `pnpm run dev:network` to start).
3535+# Fixed ports 2583 (PLC) and 2584 (PDS). PDS stays on `localhost` so it is
3636+# cross-site vs the app served on `[::1]`.
3737+DEV_PDS_URL=http://localhost:2584
3838+DEV_PLC_URL=http://localhost:2583
27392840# Lexicon Validation Configuration
2941# Enable/disable lexicon validation (default: true)
···11+import { log } from "~/lib/log";
12import { getOAuthClientMetadata } from "~/lib/oauth";
2333-export function GET() {
44+export function GET(event: { request: Request }) {
55+ log("[client-metadata] fetched by:", event.request.headers.get("user-agent") ?? "unknown");
46 const metadata = getOAuthClientMetadata();
5768 return new Response(JSON.stringify(metadata), {
+11-1
src/routes/index.tsx
···11import { Navigate, createAsync, useSubmission } from "@solidjs/router";
22-import { Show } from "solid-js";
22+import { Show, createEffect } from "solid-js";
3344import { getCurrentUser, initiateLogin } from "~/lib/queries";
55···88export default function Home() {
99 const user = createAsync(() => getCurrentUser());
1010 const loggingIn = useSubmission(initiateLogin);
1111+1212+ // The app must be served from http://[::1]:3000 (IPv6 loopback) so that the
1313+ // browser treats it as cross-site versus the PDS on http://localhost:2584.
1414+ // The atproto oauth-provider rejects same-site authorize requests with a
1515+ // 400 "Invalid request" page.
1616+ createEffect(() => {
1717+ const url = loggingIn.result?.redirectUrl;
1818+ if (!url) return;
1919+ window.location.assign(url);
2020+ });
11211222 return (
1323 <>