···11-# svelte atproto client oauth demo
11+# svelte atproto cloudflare workers oauth demo
2233-try it here: http://flo-bit.dev/svelte-atproto-client-oauth/
33+A SvelteKit app that authenticates users via AT Protocol OAuth on Cloudflare Workers. Uses server-side OAuth with `@atcute/oauth-node-client`, Cloudflare KV for session/state storage, and SvelteKit remote functions for type-safe client-server communication.
4455-this is a scaffold for how to get client side oauth working with sveltekit and atproto
66-using the [`atcute`](https://github.com/mary-ext/atcute) libraries.
55+## Prerequisites
7688-useful when you want people to login with atproto to your static sveltekit site.
77+- [Node.js](https://nodejs.org/) (v18+)
88+- [pnpm](https://pnpm.io/)
99+- A [Cloudflare account](https://dash.cloudflare.com/sign-up)
1010+- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) (`pnpm add -g wrangler`)
1111+- A domain pointed at Cloudflare (for production)
9121010-## how to install
1313+## Local Development
11141212-### either clone this repo
1515+### 1. Install dependencies
13161414-1. clone this repo
1515-2. run `pnpm install`
1616-3. run `pnpm run dev`
1717-4. go to `http://127.0.0.1:5179`
1818-5. if necessary change base in `svelte.config.js`
1717+```sh
1818+pnpm install
1919+```
19202020-```js
2121-const config = {
2222- // ...
2121+### 2. Run the dev server
23222424- kit: {
2525- // ...
2626-2727- paths: {
2828- base: process.env.NODE_ENV === 'development' ? '' : '/svelte-atproto-client-oauth'
2929- }
3030- }
3131-};
2323+```sh
2424+pnpm dev
3225```
33263434-6. change the SITE in `$lib/atproto/settings.ts` to your website
2727+In dev mode, the app uses a **loopback OAuth client** (public, no keys needed) with in-memory stores. It binds to `127.0.0.1:5183` — this is required for AT Protocol loopback OAuth.
2828+2929+No Cloudflare setup is needed for local development.
35303636-7. setup the correct permissions (see below)
3131+## Production Deployment
37323838-### or manually add to your own project
3333+### 1. Create KV namespaces
39344040-1. copy the `src/lib/atproto` folder into your own project
4141-2. also copy the `src/routes/oauth-client-metadata.json` folder into your project
4242-3. initialize the client in your `src/routes/+layout.svelte`
3535+Create two KV namespaces for storing OAuth sessions and states:
43364444-```svelte
4545-<script>
4646- import { initClient } from '$lib/atproto';
3737+```sh
3838+npx wrangler kv namespace create OAUTH_SESSIONS
3939+npx wrangler kv namespace create OAUTH_STATES
4040+```
47414848- let { children } = $props();
4242+Each command outputs an ID. Update `wrangler.toml` with the actual IDs:
49435050- onMount(() => {
5151- initClient();
5252- });
5353-</script>
4444+```toml
4545+[[kv_namespaces]]
4646+binding = "OAUTH_SESSIONS"
4747+id = "<your-sessions-namespace-id>"
54485555-{@render children()}
4949+[[kv_namespaces]]
5050+binding = "OAUTH_STATES"
5151+id = "<your-states-namespace-id>"
5652```
57535858-4. add server and port to your `vite.config.ts`
5454+### 2. Generate a client assertion key
59556060-```js
6161-export default defineConfig({
6262- server: {
6363- host: '127.0.0.1',
6464- port: 5179
6565- }
6666-});
5656+AT Protocol OAuth requires a confidential client with a private key for production. Generate one:
5757+5858+```sh
5959+pnpm generate-key
6760```
68616969-5. install the dependencies
6262+This outputs a JSON key. Add it as a Cloudflare Workers secret:
70637171-```bash
7272-npm install @atcute/atproto @atcute/bluesky @atcute/identity-resolver @atcute/lexicons @atcute/oauth-browser-client @atcute/client
6464+```sh
6565+npx wrangler secret put CLIENT_ASSERTION_KEY
7366```
74677575-6. (optionally) set your base in `svelte.config.js` (e.g. for deploying to github pages: `base: '/your-repo-name/'`) while keeping it as `''` in development.
6868+When prompted, paste the JSON key value.
76697777-```ts
7878-const config = {
7979- // ...
7070+### 3. Configure your domain
80718181- kit: {
8282- // ...
7272+Edit `src/lib/atproto/settings.ts` and set `SITE` to your production domain:
83738484- paths: {
8585- base: process.env.NODE_ENV === 'development' ? '' : '/svelte-atproto-client-oauth'
8686- }
8787- }
8888-};
7474+```ts
7575+export const SITE = dev ? 'http://localhost:5183' : 'https://your-domain.com';
8976```
90779191-6. change the SITE in `$lib/atproto/settings.ts` to your website
7878+### 4. Serve the OAuth client metadata
92799393-7. setup the correct permissions (see below)
8080+AT Protocol OAuth requires a client metadata JSON to be publicly accessible at `https://your-domain.com/oauth-client-metadata.json`. This is referenced as the `client_id`.
94819595-## how to use
8282+Create `static/oauth-client-metadata.json` with your domain info, or serve it via a route. The metadata is constructed in `src/lib/atproto/metadata.ts` — the required fields are:
96839797-### set permissions you request on sign-in in `$lib/atproto/settings.ts` (see commented out examples for more info)
8484+```json
8585+{
8686+ "client_id": "https://your-domain.com/oauth-client-metadata.json",
8787+ "redirect_uris": ["https://your-domain.com/oauth/callback"],
8888+ "scope": "atproto repo:xyz.statusphere.status",
8989+ "jwks_uri": "https://your-domain.com/oauth/jwks.json"
9090+}
9191+```
98929999-- add collections to the collections array
100100-- rpcs for authenticated proxied requests
101101-- blobs for uploading blobs
9393+The scope is auto-generated from the `permissions` config in `src/lib/atproto/settings.ts`.
10294103103-### change sign up pds
9595+### 5. Configure permissions
10496105105-If you want to allow sign-up, change the `devPDS` and `prodPDS` variables in `$lib/atproto/settings.ts` to a pds of your choice
106106-107107-ATTENTION: the current setting (pds.rip) is only for development, all accounts get deleted automatically after a week
108108-109109-### login flow
110110-111111-Either use the `LoginModal` component to render a login modal or use the `user` object to handle the login flow yourself.
9797+Edit the `permissions` object in `src/lib/atproto/settings.ts` to control what your app can do:
1129811399```ts
114114-// handlin login flow yourself
115115-import { user } from '$lib/atproto';
100100+export const permissions = {
101101+ // collections your app can create/delete/update records in
102102+ collections: ['xyz.statusphere.status'],
116103117117-// methods:
118118-user.isInitializing;
119119-user.isLoggedIn;
120120-user.login(handle);
121121-user.signup();
122122-user.logout();
104104+ // authenticated proxied RPC requests (e.g. to appview services)
105105+ rpc: {},
106106+107107+ // blob types your app can upload
108108+ blobs: []
109109+} as const;
123110```
124111125125-LoginModal is a component that renders a login modal, add it for a quick login flow (needs tailwind and tailwind/forms, copy the `src/app.css` content to your `app.css`).
112112+### 6. Deploy
126113127127-```svelte
128128-<script>
129129- import { LoginModal, loginModalState } from '$lib/atproto/ui';
130130-</script>
114114+```sh
115115+pnpm build
116116+npx wrangler deploy
117117+```
131118132132-<LoginModal />
119119+Or deploy directly:
133120134134-<button onclick={() => loginModalState.show()}>Show Login Modal</button>
121121+```sh
122122+npx wrangler deploy
135123```
136124137137-### make requests
125125+### 7. Set up a custom domain (recommended)
138126139139-Get the user's profile and make requests with the `user.client` object.
127127+In the Cloudflare dashboard, go to your Worker > Settings > Domains & Routes and add your custom domain. This is needed so the OAuth client metadata URL matches your `client_id`.
140128141141-```ts
142142-import { user } from '$lib/atproto';
129129+## Project Structure
143130144144-// make requests with the user.client object
145145-// this example needs the getActorLikes rpc permission, set permissions
146146-const response = await user.client.get('app.bsky.feed.getActorLikes', {
147147- params: {
148148- actor: client.did,
149149- limit: 10
150150- }
151151-});
131131+```
132132+src/
133133+├── lib/
134134+│ ├── atproto/
135135+│ │ ├── auth.svelte.ts # Client-side auth state (derived from server data)
136136+│ │ ├── metadata.ts # OAuth client metadata construction
137137+│ │ ├── methods.ts # AT Protocol helper methods
138138+│ │ ├── oauth.remote.ts # Remote functions: login, logout
139139+│ │ ├── repo.remote.ts # Remote functions: putRecord, deleteRecord, uploadBlob
140140+│ │ └── settings.ts # Site URL, permissions, constants
141141+│ └── server/
142142+│ ├── oauth.ts # OAuthClient factory (dev vs prod)
143143+│ └── kv-store.ts # Cloudflare KV-backed Store implementation
144144+├── routes/
145145+│ ├── oauth/
146146+│ │ ├── callback/ # OAuth callback handler (GET redirect)
147147+│ │ └── jwks.json/ # Public JWKS endpoint
148148+│ ├── +layout.server.ts # Loads user profile from session
149149+│ ├── +layout.svelte
150150+│ ├── +page.server.ts # Loads page-specific data
151151+│ └── +page.svelte
152152+└── hooks.server.ts # Session restoration from cookie
152153```
153154154154-## todo
155155+## How It Works
156156+157157+- **Authentication**: Server-side OAuth flow via `@atcute/oauth-node-client`. Sessions are stored in Cloudflare KV and identified by a `did` cookie.
158158+- **Remote functions**: Write operations (putRecord, deleteRecord, uploadBlob) and auth actions (login, logout) use SvelteKit remote functions — type-safe server calls without manual API routes.
159159+- **Dev mode**: Uses AT Protocol's loopback client (no keys, in-memory storage).
160160+- **Prod mode**: Uses a confidential client with `private_key_jwt` assertion and KV-backed stores.
155161156156-- check if pds supports prompt=create
157157-- add lexicon stuff
162162+## License
163163+164164+MIT