[READ-ONLY] Mirror of https://github.com/flo-bit/svelte-cloudflare-statusphere. statusphere.atmo.tools
atproto cloudflare-workers oauth svelte
0

Configure Feed

Select the types of activity you want to include in your feed.

1# Add AT Protocol OAuth to SvelteKit + Cloudflare Workers 2 3You are adding AT Protocol OAuth authentication to an existing SvelteKit project deployed on Cloudflare Workers. This uses server-side OAuth with `@atcute/oauth-node-client`, Cloudflare KV for session storage, and SvelteKit remote functions. 4 5## Prerequisites 6 7The project must already use: 8- SvelteKit with `@sveltejs/adapter-cloudflare` 9- A `wrangler.jsonc` (or `wrangler.toml`) config 10 11## Step 0: Ask the user 12 13Before making any changes, ask the user these questions: 14 151. **UI**: Should I add a login UI? 16 - **`foxui`** — Use `@foxui/social` login modal (polished, recommended) 17 - **`basic`** — Simple login/logout page at `/user` route (uses Tailwind if available) 18 - **`none`** — Backend only, no UI (you'll build your own) 19 202. **Collections**: What AT Protocol collections should your app write to? (e.g. `xyz.statusphere.status`, `app.bsky.feed.like`). Leave empty for read-only. 21 223. **Blobs**: Does the app need to upload blobs (images, video)? If yes, what types? (e.g. `image/*`, `video/*`) 23 244. **Signup**: Should the app allow users to create new AT Protocol accounts (signup)? 25 - **`yes`** — Include a signup button/flow 26 - **`no`** — Login only, no account creation 27 285. **Production PDS**: Which PDS should be used for signup in production? (default: `https://selfhosted.social/`) 29 - Only relevant if signup is enabled. Skip if signup is `no`. 30 31Use the answers to customize `settings.ts` (marked with `CUSTOMIZE` below) and choose which UI dependencies/files to create. 32 33## Step 1: Install dependencies 34 35Always install: 36 37```sh 38pnpm add valibot 39pnpm add -D @atcute/oauth-node-client @atcute/identity-resolver @atcute/lexicons @atcute/client @atcute/tid @cloudflare/workers-types tsx @atcute/atproto @atcute/bluesky 40``` 41 42If UI choice is `foxui`: 43 44```sh 45pnpm add @foxui/social @foxui/core 46``` 47 48## Step 2: Create files 49 50### Download all files 51 52Run this bash script to download all required files into the project: 53 54```sh 55BASE_URL="https://raw.githubusercontent.com/flo-bit/atproto-oauth-cloudflare/main" 56 57FILES=( 58 "src/lib/atproto/auth.svelte.ts" 59 "src/lib/atproto/methods.ts" 60 "src/lib/atproto/image-helper.ts" 61 "src/lib/atproto/index.ts" 62 "src/lib/atproto/server/signed-cookie.ts" 63 "src/lib/atproto/server/kv-store.ts" 64 "src/lib/atproto/server/oauth.ts" 65 "src/lib/atproto/server/oauth.remote.ts" 66 "src/lib/atproto/server/repo.remote.ts" 67 "src/lib/atproto/server/session.ts" 68 "src/lib/atproto/server/profile.ts" 69 "src/lib/atproto/scripts/generate-key.ts" 70 "src/lib/atproto/scripts/generate-secret.ts" 71 "src/lib/atproto/scripts/setup-dev.ts" 72 "src/routes/(oauth)/oauth/callback/+server.ts" 73 "src/routes/(oauth)/oauth/jwks.json/+server.ts" 74 "src/routes/(oauth)/oauth-client-metadata.json/+server.ts" 75 ".env.example" 76) 77 78for file in "${FILES[@]}"; do 79 mkdir -p "$(dirname "$file")" 80 curl -fsSL "$BASE_URL/$file" -o "$file" 81 echo " downloaded $file" 82done 83``` 84 85### `src/lib/atproto/settings.ts` 86 87**Do not fetch this file.** Create it manually using the template below, customized with the user's answers: 88 89```ts 90import { dev } from '$app/environment'; 91import { scope } from '@atcute/oauth-node-client'; 92 93// CUSTOMIZE: writable collections 94export const collections = [] as const; 95 96export type AllowedCollection = (typeof collections)[number]; 97 98// CUSTOMIZE: OAuth scope — add scope.blob({ accept: ['image/*'] }), scope.rpc(), etc. as needed 99export const scopes = ['atproto', scope.repo({ collection: [...collections] })]; 100 101// CUSTOMIZE: set to true to allow signup, false for login-only 102export const ALLOW_SIGNUP = true; 103 104// CUSTOMIZE: PDS to use for signup (only relevant if ALLOW_SIGNUP is true) 105const devPDS = 'https://bsky.social/'; 106const prodPDS = 'https://selfhosted.social/'; // CUSTOMIZE: change to preferred production PDS 107export const signUpPDS = dev ? devPDS : prodPDS; 108 109export const REDIRECT_PATH = '/oauth/callback'; 110 111// redirect the user back to the page they were on before login 112export const REDIRECT_TO_LAST_PAGE_ON_LOGIN = true; 113 114export const DOH_RESOLVER = 'https://mozilla.cloudflare-dns.com/dns-query'; 115``` 116 117## Step 3: Modify existing files 118 119### `src/app.d.ts` 120 121Add these to the existing `App` namespace. Merge with any existing `Locals` or `Platform` fields — do not remove existing fields. 122 123```ts 124import type { OAuthSession } from '@atcute/oauth-node-client'; 125import type { Client } from '@atcute/client'; 126import type { Did } from '@atcute/lexicons'; 127``` 128 129Add to `App.Locals`: 130 131```ts 132session: OAuthSession | null; 133client: Client | null; 134did: Did | null; 135``` 136 137Add to `App.Platform`: 138 139```ts 140env: { 141 OAUTH_SESSIONS: KVNamespace; 142 OAUTH_STATES: KVNamespace; 143 CLIENT_ASSERTION_KEY: string; 144 COOKIE_SECRET: string; 145 OAUTH_PUBLIC_URL: string; 146 PROFILE_CACHE?: KVNamespace; 147}; 148``` 149 150Add at the bottom of the file (for lexicon type augmentation): 151 152```ts 153import type {} from '@atcute/atproto'; 154import type {} from '@atcute/bluesky'; 155``` 156 157### `src/hooks.server.ts` 158 159Add session restoration. If the file already has a `handle` export, wrap both in `sequence()` from `@sveltejs/kit`. 160 161```ts 162import type { Handle } from '@sveltejs/kit'; 163import { restoreSession } from '$lib/atproto/server/session'; 164 165const atprotoHandle: Handle = async ({ event, resolve }) => { 166 const { session, client, did } = await restoreSession( 167 event.cookies, event.platform?.env 168 ); 169 event.locals.session = session; 170 event.locals.client = client; 171 event.locals.did = did; 172 return resolve(event); 173}; 174``` 175 176If no existing hooks: `export const handle = atprotoHandle;` 177 178If existing hooks: `export const handle = sequence(existingHandle, atprotoHandle);` (import `sequence` from `@sveltejs/kit`) 179 180### `src/routes/+layout.server.ts` 181 182Add profile loading. Merge with any existing load function. 183 184```ts 185import type { LayoutServerLoad } from './$types'; 186import { loadProfile } from '$lib/atproto/server/profile'; 187 188export const load: LayoutServerLoad = async ({ locals, platform }) => { 189 if (!locals.did) return { did: null, profile: null }; 190 const profile = await loadProfile(locals.did, platform?.env?.PROFILE_CACHE); 191 return { did: locals.did, profile }; 192}; 193``` 194 195If a load function already exists, merge the profile data into its return value. 196 197### `src/routes/+layout.svelte` (foxui only) 198 199Only if the user chose `foxui`. Add the login modal to the existing layout. 200 201If signup is enabled (`ALLOW_SIGNUP = true`): 202 203```svelte 204<script lang="ts"> 205 import { AtprotoLoginModal } from '@foxui/social'; 206 import { login, signup } from '$lib/atproto'; 207</script> 208 209<!-- keep existing layout content, add this at the bottom: --> 210<AtprotoLoginModal 211 login={async (handle) => { 212 await login(handle); 213 return true; 214 }} 215 signup={async () => { 216 signup(); 217 return true; 218 }} 219/> 220``` 221 222If signup is disabled (`ALLOW_SIGNUP = false`), omit the `signup` prop: 223 224```svelte 225<script lang="ts"> 226 import { AtprotoLoginModal } from '@foxui/social'; 227 import { login } from '$lib/atproto'; 228</script> 229 230<AtprotoLoginModal 231 login={async (handle) => { 232 await login(handle); 233 return true; 234 }} 235/> 236``` 237 238To show the modal from anywhere, use `@foxui/social` state and `@foxui/core` components: 239 240```svelte 241<script lang="ts"> 242 import { Button } from '@foxui/core'; 243 import { atProtoLoginModalState } from '@foxui/social'; 244 import { user, logout } from '$lib/atproto'; 245</script> 246 247{#if user.isLoggedIn} 248 <p>Signed in as {user.profile?.handle ?? user.did}</p> 249 <Button onclick={() => logout()}>Sign Out</Button> 250{:else} 251 <Button onclick={() => atProtoLoginModalState.show()}>Sign In</Button> 252{/if} 253``` 254 255`@foxui/core` also exports `Avatar`, `Input`, and other UI primitives you can use. 256 257### `src/routes/user/+page.svelte` (basic only) 258 259Only if the user chose `basic`. Create this file: 260 261```svelte 262<script lang="ts"> 263 import { user, login, logout } from '$lib/atproto'; 264 265 let handle = $state(''); 266 let error = $state(''); 267 let loading = $state(false); 268 269 async function handleLogin() { 270 if (!handle.trim()) return; 271 loading = true; 272 error = ''; 273 try { 274 await login(handle); 275 } catch (e) { 276 error = e instanceof Error ? e.message : 'Login failed'; 277 loading = false; 278 } 279 } 280</script> 281 282<div class="mx-auto max-w-sm p-8"> 283 {#if user.isLoggedIn} 284 <p class="mb-4">Signed in as <strong>{user.profile?.handle ?? user.did}</strong></p> 285 <button 286 class="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700" 287 onclick={() => logout()} 288 > 289 Sign Out 290 </button> 291 {:else} 292 <h1 class="mb-4 text-xl font-bold">Sign in</h1> 293 <form onsubmit={handleLogin} class="flex flex-col gap-3"> 294 <input 295 type="text" 296 bind:value={handle} 297 placeholder="handle.bsky.social" 298 class="rounded border px-3 py-2" 299 disabled={loading} 300 /> 301 {#if error} 302 <p class="text-sm text-red-600">{error}</p> 303 {/if} 304 <button 305 type="submit" 306 class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50" 307 disabled={loading || !handle.trim()} 308 > 309 {loading ? 'Signing in...' : 'Sign In'} 310 </button> 311 </form> 312 {/if} 313</div> 314``` 315 316If the project does not use Tailwind, replace the Tailwind classes with plain inline styles. 317 318### `svelte.config.js` 319 320Add `remoteFunctions: true` inside `kit.experimental`: 321 322```js 323kit: { 324 adapter: adapter(), 325 experimental: { 326 remoteFunctions: true 327 } 328} 329``` 330 331If `experimental` already exists, merge into it. Do not remove other experimental flags. 332 333### `vite.config.ts` 334 335Add dev server config for loopback OAuth: 336 337```ts 338server: { 339 host: '127.0.0.1', 340 port: 5183 341} 342``` 343 344Add this inside `defineConfig()`. Do not remove existing plugins or config. 345 346### `wrangler.jsonc` 347 348Add or merge these fields: 349 350- Add `"nodejs_compat_v2"` to `compatibility_flags` (create the array if it doesn't exist) 351- Do NOT add `OAUTH_PUBLIC_URL` to vars — it is only needed for production deployment and the user will set it themselves later. In dev mode without it, the app uses a loopback client automatically. 352- Add KV namespace placeholders to `kv_namespaces`: 353 354```jsonc 355{ "binding": "OAUTH_SESSIONS", "id": "TODO" }, 356{ "binding": "OAUTH_STATES", "id": "TODO" } 357``` 358 359Do not remove existing bindings or vars. 360 361### `tsconfig.json` 362 363Add `"@cloudflare/workers-types"` to `compilerOptions.types`. Create the `types` array if it doesn't exist. 364 365### `package.json` 366 367Add these to the `scripts` section: 368 369```json 370"env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", 371"env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", 372"env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts" 373``` 374 375### `.gitignore` 376 377Ensure these lines are present: 378 379``` 380.env 381.env.* 382!.env.example 383``` 384 385## Step 4: Run setup and verify 386 3871. Run `pnpm env:setup-dev` to generate secrets in `.env` 3882. Run `pnpm dev` to start the dev server 3893. Verify it starts on `http://127.0.0.1:5183` 3904. Tell the user: 391 - Dev mode uses a loopback client (no keys needed) 392 - For production: create KV namespaces with `npx wrangler kv namespace create OAUTH_SESSIONS` and `OAUTH_STATES`, update the IDs in `wrangler.jsonc`, set `OAUTH_PUBLIC_URL` to their domain, and run `npx wrangler secret put CLIENT_ASSERTION_KEY` / `COOKIE_SECRET` with values from `pnpm env:generate-key` / `pnpm env:generate-secret` 393 394## Usage examples 395 396### Login / Logout 397 398```svelte 399<script lang="ts"> 400 import { user, login, logout } from '$lib/atproto'; 401</script> 402 403{#if user.isLoggedIn} 404 <p>Signed in as {user.did}</p> 405 <button onclick={() => logout()}>Sign Out</button> 406{:else} 407 <button onclick={() => login('user.bsky.social')}>Sign In</button> 408{/if} 409``` 410 411### Write operations 412 413```ts 414import { putRecord, deleteRecord, uploadBlob, createTID } from '$lib/atproto'; 415 416await putRecord({ 417 collection: 'your.collection.name', 418 rkey: createTID(), 419 record: { text: 'hello', createdAt: new Date().toISOString() } 420}); 421 422await deleteRecord({ collection: 'your.collection.name', rkey: 'some-key' }); 423 424const blob = await uploadBlob({ blob: file }); 425``` 426 427### Read operations (no auth needed) 428 429```ts 430import { listRecords, getRecord, getDetailedProfile } from '$lib/atproto'; 431 432const records = await listRecords({ did: 'did:plc:...', collection: 'your.collection.name' }); 433const profile = await getDetailedProfile({ did: 'did:plc:...' }); 434```