···
643
-
Let's create the client during the server init:
647
-
import { NodeOAuthClient } from '@atproto/oauth-client-node'
649
-
// static async create() {
652
-
const publicUrl = env.PUBLIC_URL
653
-
const url = publicUrl || `http://127.0.0.1:${env.PORT}`
654
-
const oauthClient = new NodeOAuthClient({
656
-
client_name: 'AT Protocol Express App',
657
-
client_id: publicUrl
658
-
? `${url}/client-metadata.json`
659
-
: `http://localhost?redirect_uri=${encodeURIComponent(`${url}/oauth/callback`)}`,
661
-
redirect_uris: [`${url}/oauth/callback`],
662
-
scope: 'profile offline_access',
663
-
grant_types: ['authorization_code', 'refresh_token'],
664
-
response_types: ['code'],
665
-
application_type: 'web',
666
-
token_endpoint_auth_method: 'none',
667
-
dpop_bound_access_tokens: true,
669
-
stateStore: new StateStore(db),
670
-
sessionStore: new SessionStore(db),
677
-
There's quite a bit of configuration which is [explained in the OAuth guide](#todo). We host that config at `/client-metadata.json` as part of the OAuth flow.
680
-
/** src/routes.ts **/
684
-
'/client-metadata.json',
685
-
handler((_req, res) => {
686
-
return res.json(oauthClient.clientMetadata)
695
-
We're going to need to track two kinds of information:
697
-
- **OAuth State**. This is information about login flows that are in-progress.
698
-
- **OAuth Sessions**. This is the active session data.
700
-
The `oauth-client-node` library handles most of this for us, but we need to create some tables in our SQLite to store it. Let's update `/src/db.ts` for this.
706
-
export type DatabaseSchema = {
707
-
auth_session: AuthSession
708
-
auth_state: AuthState
711
-
export type AuthSession = {
713
-
session: string // JSON
716
-
export type AuthState = {
718
-
state: string // JSON
723
-
migrations['001'] = {
724
-
async up(db: Kysely<unknown>) {
726
-
.createTable('auth_session')
727
-
.addColumn('key', 'varchar', (col) => col.primaryKey())
728
-
.addColumn('session', 'varchar', (col) => col.notNull())
731
-
.createTable('auth_state')
732
-
.addColumn('key', 'varchar', (col) => col.primaryKey())
733
-
.addColumn('state', 'varchar', (col) => col.notNull())
736
-
async down(db: Kysely<unknown>) {
737
-
await db.schema.dropTable('auth_state').execute()
738
-
await db.schema.dropTable('auth_session').execute()
749
-
Data in the Atmosphere is stored on users' personal servers. It's almost like each user has their own website. Our goal is to aggregate data from the users into our SQLite.
751
-
Think of our app like a Google. If Google's job was to say which emoji each website had under `/status.txt`, then it would show something like:
753
-
- `nytimes.com` is feeling 📰 according to `https://nytimes.com/status.txt`
754
-
- `bsky.app` is feeling 🦋 according to `https://bsky.app/status.txt`
755
-
- `reddit.com` is feeling 🤓 according to `https://reddit.com/status.txt`
757
-
The Atmosphere works the same way, except we're going to check `at://nytimes.com/com.example.status/self`. Literally, that's it! Each user has a domain, and each record gets published under an atproto URL.
762
-
at://nytimes.com/com.example.status/self
764
-
The user The data type The record name
767
-
When somebody logs into our app, they'll give read & write access to their personal `at://`. We'll use that to write the `/com.example.status/self` record. Then we'll crawl the Atmosphere for all the other `/com.example.status/self` records, and aggregate them into our SQLite database for fast reads.
769
-
Believe it or not, that's how most apps on the Atmosphere are built, including [Bluesky](#todo).