WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import type { Database } from "@atbb/db";
2import { createDb } from "@atbb/db";
3import type { Logger } from "@atbb/logger";
4import { createLogger } from "@atbb/logger";
5import { FirehoseService } from "./firehose.js";
6import { NodeOAuthClient } from "@atproto/oauth-client-node";
7import { OAuthStateStore, OAuthSessionStore } from "./oauth-stores.js";
8import { CookieSessionStore } from "./cookie-session-store.js";
9import { ForumAgent } from "@atbb/atproto";
10import type { AppConfig } from "./config.js";
11import { BackfillManager } from "./backfill-manager.js";
12
13/**
14 * Application context holding all shared dependencies.
15 * This interface defines the contract for dependency injection.
16 */
17export interface AppContext {
18 config: AppConfig;
19 logger: Logger;
20 db: Database;
21 firehose: FirehoseService;
22 oauthClient: NodeOAuthClient;
23 oauthStateStore: OAuthStateStore;
24 oauthSessionStore: OAuthSessionStore;
25 cookieSessionStore: CookieSessionStore;
26 forumAgent: ForumAgent | null;
27 backfillManager: BackfillManager | null;
28}
29
30/**
31 * Create and initialize the application context with all dependencies.
32 * This is the composition root where we wire up all dependencies.
33 */
34export async function createAppContext(config: AppConfig): Promise<AppContext> {
35 const logger = createLogger({
36 service: "atbb-appview",
37 version: "0.1.0",
38 environment: process.env.NODE_ENV ?? "development",
39 level: config.logLevel,
40 });
41
42 const db = createDb(config.databaseUrl);
43 const firehose = new FirehoseService(db, config.jetstreamUrl, logger);
44
45 // Initialize OAuth stores
46 const oauthStateStore = new OAuthStateStore();
47 const oauthSessionStore = new OAuthSessionStore();
48 const cookieSessionStore = new CookieSessionStore();
49
50 // Simple in-memory lock for single-instance deployments
51 // For multi-instance production, use Redis-based locking (e.g., with redlock)
52 const locks = new Map<string, Promise<unknown>>();
53 const requestLock = async <T>(key: string, fn: () => T | PromiseLike<T>): Promise<T> => {
54 // Wait for any existing lock on this key
55 while (locks.has(key)) {
56 await locks.get(key);
57 }
58
59 // Acquire lock
60 const promise = Promise.resolve(fn());
61 locks.set(key, promise);
62
63 try {
64 return await promise;
65 } finally {
66 // Release lock
67 locks.delete(key);
68 }
69 };
70
71 // Replace localhost with 127.0.0.1 for RFC 8252 compliance
72 const oauthUrl = config.oauthPublicUrl.replace('localhost', '127.0.0.1');
73
74 // Initialize OAuth client with configuration
75 const oauthClient = new NodeOAuthClient({
76 clientMetadata: {
77 client_id: `${oauthUrl}/.well-known/oauth-client-metadata`,
78 client_name: "atBB Forum",
79 client_uri: oauthUrl,
80 redirect_uris: [`${oauthUrl}/api/auth/callback`],
81 scope: "atproto transition:generic",
82 grant_types: ["authorization_code", "refresh_token"],
83 response_types: ["code"],
84 application_type: "web",
85 token_endpoint_auth_method: "none",
86 dpop_bound_access_tokens: true,
87 },
88 stateStore: oauthStateStore,
89 sessionStore: oauthSessionStore,
90 requestLock,
91 // Allow HTTP for development (never use in production!)
92 allowHttp: process.env.NODE_ENV !== "production",
93 });
94
95 // Initialize ForumAgent (soft failure - never throws)
96 let forumAgent: ForumAgent | null = null;
97 if (config.forumHandle && config.forumPassword) {
98 forumAgent = new ForumAgent(
99 config.pdsUrl,
100 config.forumHandle,
101 config.forumPassword,
102 logger
103 );
104 await forumAgent.initialize();
105 } else {
106 logger.warn("ForumAgent credentials missing", {
107 operation: "createAppContext",
108 reason: "Missing FORUM_HANDLE or FORUM_PASSWORD environment variables",
109 });
110 }
111
112 return {
113 config,
114 logger,
115 db,
116 firehose,
117 oauthClient,
118 oauthStateStore,
119 oauthSessionStore,
120 cookieSessionStore,
121 forumAgent,
122 backfillManager: new BackfillManager(db, config, logger),
123 };
124}
125
126/**
127 * Cleanup and release resources held by the application context.
128 */
129export async function destroyAppContext(ctx: AppContext): Promise<void> {
130 await ctx.firehose.stop();
131
132 if (ctx.forumAgent) {
133 await ctx.forumAgent.shutdown();
134 }
135
136 // Clean up OAuth store timers
137 ctx.oauthStateStore.destroy();
138 ctx.oauthSessionStore.destroy();
139 ctx.cookieSessionStore.destroy();
140
141 // Flush pending log records and release OTel resources
142 await ctx.logger.shutdown();
143}