.tangled/images/oauth-no-scope.png
.tangled/images/oauth-no-scope.png
This is a binary file and will not be displayed.
+2
Dockerfile
+2
Dockerfile
+21
LICENSE.md
+21
LICENSE.md
···
1
+
MIT License
2
+
3
+
Copyright (c) 2025 Bailey Townsend
4
+
5
+
Permission is hereby granted, free of charge, to any person obtaining a copy
6
+
of this software and associated documentation files (the "Software"), to deal
7
+
in the Software without restriction, including without limitation the rights
8
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+
copies of the Software, and to permit persons to whom the Software is
10
+
furnished to do so, subject to the following conditions:
11
+
12
+
The above copyright notice and this permission notice shall be included in all
13
+
copies or substantial portions of the Software.
14
+
15
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+
SOFTWARE.
+55
-6
README.md
+55
-6
README.md
···
5
5
6
6
## Features
7
7
- OAuth client configured from `.env` variables
8
-
- `DEV=true` allows local development, do not need a public url
9
-
- removing `DEV=true` and setting `OAUTH_DOMAIN` is production and requires a public url like [demo.atpoke.xyz](https://demo.atpoke.xyz)
10
-
- Setting `OAUTH_JWK` allows for the confidential client that lasts much longer (forever if you refresh the tokens, 180 for indivudal refresh tokens). Can get a value for it from running `node ./bin/gen-jwk.js`
11
8
- Server side sessions and automatic loading of the atproto client from `event.locals.atpAgent` on server side components.
12
9
- Examples on how to create atproto records with [pokes or making a Bluesky post](./src/routes/demo/+page.server.ts)
13
10
- Examples showing how to use [microcosm.blue](https://microcosm.blue/) tooling for an appview like experince without the appview.
···
15
12
- Find the [handles from the did](./src/routes/demo/+page.svelte) easily with [slingshot](https://slingshot.microcosm.blue/)
16
13
17
14
## Dev Setup
18
-
1. Copy [.env.example](.env.example) to [.env](.env), .env.example is dev settings
15
+
Should work with any package manager if you prefer to use another. But these are directions for [pnpm](https://pnpm.io/)
16
+
17
+
1. Copy [.env.example](.env.example) to [.env](.env), .env.example is the default dev settings
19
18
2. `pnpm install`
20
19
3. May need to run `pnpm approve-builds` for the build scripts for sqlite
21
20
4. `pnpm run dev` or `pnpm run dev:logging` with [pino-pretty](https://github.com/pinojs/pino-pretty) for pretty logging
22
21
23
22
> If you are running locally on a different port than `5173` or something else odd can set `OAUTH_DOMAIN` .env to the domain and port. Just make sure to use either `127.0.0.1` or `[::1]`(ipv6) for oauth to work for local development.
24
23
24
+
## Implementation details
25
+
Details on some of the implementation details that you will most likely want to edit when creating your own project.
26
+
27
+
### How to configure OAuth
28
+
OAuth *should* Already be figured out for you with everything being configured from the environment variables without having to modify any code.
29
+
30
+
> All atproto actions taken for the user happen server side, so we can take advantage of the confidential client and longer session lifetimes.
31
+
32
+
## Types of OAuth clients
33
+
- Development local - Set `DEV=true` in `.env` to use a local development client. This is a special client just for development does not require having a public url. As outlined [here](https://atproto.com/specs/oauth#localhost-client-development). This is the default from copying the [.env.example](.env.example)
34
+
- Production Public – Remove the `DEV=true`, Set `OAUTH_DOMAIN` to your publicliy accessible domain([demo.atpoke.xyz](https://demo.atpoke.xyz) in `.env` to use a production public client. These have a lower atproto oauth session lifetime which is limited to 2 weeks.
35
+
- Production Confidential - Follow `Production Public` and set `OAUTH_JWK` to the value from `node ./bin/gen-jwk.js`. **These are cryptographic signing keys and should be kept secret and private**. These have the longest session lifetime of 180 days for refresh tokens, or indefinitely if refreshed till, pending revocation or jwk rotation.
36
+
37
+
Most likely in production you are going to want the Confidential client for the longest lifetime.
38
+
The cookie session lifetime is less, so this could expire before the atproto session lifetime. This can be configured at [./src/lib/server/session.ts](./src/lib/server/session.ts), default is 30 days and resets for every logged-in web action. Can read more on atproto clients with [Client type details](https://atproto.com/specs/oauth#types-of-clients) and [session lifetime details](https://atproto.com/specs/oauth#tokens-and-session-lifetime)
39
+
40
+
## OAuth scopes
41
+
OAuth scopes are permissions to the user's repo you are requesting. You can read more on them and learn the different ones on [atproto.com/specs/permission](https://atproto.com/specs/permission)
42
+
43
+
This demo application only requires the access to the lexicons it needs with default scopes of `atproto repo:app.bsky.feed.post?action=create repo:xyz.atpoke.graph.poke`. This can be changed by setting the `OAUTH_SCOPES` environment variable. For development or full access to a user's repo you are probably looking for `atproto transition:generic`
44
+

45
+
46
+
> You may find as you're trying out new scopes that the OAuth screen may not reflect what you've requested. This is because the PDS can cache those. [Docs say this can be anywehre from 15-30mins](https://atproto.com/specs/permission#resolution-and-caching)
47
+
48
+
## OAuth Branding
49
+
There are a couple of other odds and ends you can set for OAuth to customize the branding. This mostly shows up on `yourpds.com/account` for now, but it is a standard and more may adpot it. Will be taking the [docs definitions](https://atproto.com/specs/oauth#client-id-metadata-document) for each.
50
+
51
+
- `OAUTH_CLIENT_NAME` - (string, optional): human-readable name of the client
52
+
- `OAUTH_LOGO_URI` - (string, optional): URL to client logo. Only https: URIs are allowed.
53
+
- `OAUTH_TOS_URI` - (string, optional): URL to human-readable terms of service (ToS) for the client. Only https: URIs are allowed.
54
+
- `OAUTH_POLICY_URI` - (string, optional): URL to human-readable privacy policy for the client. Only https: URIs are allowed.
55
+
56
+
### Database
57
+
This project uses [drizzle ORM](https://orm.drizzle.team/) with the sqlite adapter to make it easy to run locally and get started. This is used for the session store for the server and atproto. This will work for production as well and can build on it as is, but if you want to change out the database layer it should not be too bad since there's not a ton of db queries right now. This is a quick overview of that layer.
58
+
59
+
- [./src/lib/server/db/index.ts](./src/lib/server/db/index.ts). This sets up the DB
60
+
- [./src/lib/server/db/schema.ts](./src/lib/server/db/schema.ts). This is your database schema.
61
+
- [./src/lib/server/cache.ts](./src/lib/server/cache.ts). This is an abstracted key/value cache that the [@atproto/oauth-client-node](https://www.npmjs.com/package/@atproto/oauth-client-node?activeTab=readme) uses to store state and sessions.
62
+
- [./src/lib/server/session.ts](./src/lib/server/session.ts). This is the server side session store that is tied to the cookie session.
63
+
- A tiny job runs at [./src/hooks.server.ts](./src/hooks.server.ts) that clears the atproto state store, and runs migrations on startup. If you change from a node server adapter may have to change this as well.
64
+
- Will most likely need to change out some settings in [drizzle.config.ts](drizzle.config.ts)
65
+
- Delete the contents in [drizzle](drizzle) and run `pnpm run db:generate` to generate new migration files for the new adapter.
66
+
67
+
68
+
69
+
### Other production considerations
70
+
I did not find a great way to run a "sidecar process" with SvelteKit, and by that I mean a [Jetstream listener](https://docs.bsky.app/blog/jetstream) that runs alongside the SvelteKit application. If you are needing to listen to the firehose or jetstream to get real time records being created, I'd recommend changing out the database layered to something not embedded, then run a separate container (or process) with a node script running in a loop to get that data. [@atcute/jetstream](https://tangled.org/mary.my.id/atcute/tree/trunk/packages/clients/jetstream) is a great way to do this in TypeScript.
71
+
For an overview of what that gets you and why you would want that I'd recommend checking out the quick start guide [Statusphere](https://atproto.com/guides/applications) to see how it is used there and why.
72
+
73
+
25
74
## Production
26
75
27
76
> Sign up for Railway with my referral code [z49xDi](https://railway.com?referralCode=z49xDi) to get $20 in credits, and if you spend anything, I get 15% in credits.
···
30
79
1. Install the railway cli ([directions here](https://docs.railway.com/guides/cli#installing-the-cli))
31
80
2. Login with `railway login`
32
81
3. Create a new project with `railway init`, set your project name
33
-
4. Deploy your webapp with `railway up`, this will create a new deployment. This will crash on the first run since we still have some changes to make. This is what uploads your code to railway. That is expected since we don't have a volume and our variables yet.
34
-

82
+
4. Deploy your webapp with `railway up`, this will create a new deployment. This will crash on the first run since we still have some changes to make. That is expected since we don't have a volume and our variables yet. This is what actually uploads your code to railway via the [dockerfile](Dockerfile).
83
+

35
84
5. `railway service` select the service you deployed earlier, name is most likely the same as the project name.
36
85
6. Run `railway volume add -m /app_data` to create a persistent volume for the sqlite database.
37
86
7. If you do not already have your project dashboard open you can open it with `railway open`, this opens it in a web browser.
+3
-1
src/lib/server/atproto/client.ts
+3
-1
src/lib/server/atproto/client.ts
···
61
61
token_endpoint_auth_method: isConfidential ? 'private_key_jwt' : 'none',
62
62
dpop_bound_access_tokens: true,
63
63
jwks_uri: isConfidential ? `${rootUrl}/.well-known/jwks.json` : undefined,
64
-
token_endpoint_auth_signing_alg: isConfidential ? pk?.alg : undefined
64
+
token_endpoint_auth_signing_alg: isConfidential ? pk?.alg : undefined,
65
+
tos_uri: env.OAUTH_TOS_URI,
66
+
policy_uri: env.OAUTH_POLICY_URI,
65
67
};
66
68
67
69
return new NodeOAuthClient({
+6
src/routes/+layout.svelte
+6
src/routes/+layout.svelte
···
33
33
34
34
<svelte:head>
35
35
<link rel="icon" href={favicon} />
36
+
<title>SvelteKit Atproto Demo</title>
37
+
<meta property="og:title" content="SvelteKit Atproto Demo" />
38
+
<meta property="og:description"
39
+
content="A demo application with pokes." />
40
+
<meta property="description"
41
+
content="A demo application with pokes." />
36
42
</svelte:head>
37
43
38
44
{#if data.session}
+8
-1
src/routes/+page.svelte
+8
-1
src/routes/+page.svelte
···
12
12
<div>
13
13
<h1>SvelteKit ATProtocol OAuth template</h1>
14
14
<h3>A build your own ATProto adventure</h3>
15
-
<p> It's up to you on what this project should be and what it looks like. This is mostly just a technical preview showing a demo of what the application looks like and how it works. But if you <a href="/login">Login</a> you can poke people or see who poked you, so there's that at least.</p>
15
+
<p> It's up to you on what this project should be and what it looks like. Fork the project on <a href="https://tangled.org/baileytownsend.dev/atproto-sveltekit-template">tangled.org</a> to make it your own. This web hosted version is mostly just a technical preview showing a demo of what the application looks like and how it works, not a lot here, or even styling that's up to you, not me. But if you <a href="/login">Login</a> you can poke people or see who poked you, so there's that at least.</p>
16
16
<h3>A bare minimal SvelteKit demo that...</h3>
17
17
<ul>
18
18
<li>Can have local dev oauth with setting the <code>.env</code> variable <code>DEV=true</code></li>
···
22
22
<li>A persistent session store using <a href="https://orm.drizzle.team/">drizzle</a> with sqlite</li>
23
23
<li>A docker compose and documentation on how to deploy to <a href="https://railway.com/">railway</a></li>
24
24
<li><a href="/demo">An example page using the atproto Agent where you can make a post or poke someone</a> </li>
25
+
<li>Uses <a href="https://microcosm.blue" class="link"><span
26
+
style="color: rgb(243, 150, 169);">m</span><span style="color: rgb(244, 156, 92);">i</span><span
27
+
style="color: rgb(199, 176, 76);">c</span><span style="color: rgb(146, 190, 76);">r</span><span
28
+
style="color: rgb(78, 198, 136);">o</span><span style="color: rgb(81, 194, 182);">c</span><span
29
+
style="color: rgb(84, 190, 215);">o</span><span style="color: rgb(143, 177, 241);">s</span><span
30
+
style="color: rgb(206, 157, 241);">m</span></a> for demoing some AppView-less features you can do, like seeing who has poked you without a backend.
31
+
</li>
25
32
<li>Source code can be found on <a href="https://tangled.org/baileytownsend.dev/atproto-sveltekit-template">tangled.org</a> </li>
26
33
</ul>
27
34
<br/>
+5
src/routes/demo/+page.server.ts
+5
src/routes/demo/+page.server.ts
···
30
30
text: rt.text,
31
31
facets: rt.facets,
32
32
createdAt: new Date().toISOString(),
33
+
//This sets the language of the post. Want to most likely get it from a locale of the browser
34
+
//cheating here and using english since the rest of the documentations in english and this is a demo
35
+
langs: [
36
+
'en'
37
+
]
33
38
};
34
39
35
40
const result = await agent.com.atproto.repo.createRecord({