alf: the atproto Latency Fabric
alf.fly.dev/
1// ABOUTME: Configuration management for ALF (Atproto Latency Fabric) service
2
3import dotenv from 'dotenv';
4
5// Load environment variables
6dotenv.config();
7
8export interface ServiceConfig {
9 port: number;
10 serviceUrl: string;
11 plcRoot: string;
12 /** URL for resolving ATProto handles (e.g. a PDS or AppView). Defaults to Bsky AppView. */
13 handleResolverUrl: string;
14 databaseType: 'sqlite' | 'postgres';
15 databasePath: string;
16 databaseUrl?: string;
17 encryptionKey: string;
18 /** Optional URL to POST to after a draft is successfully published */
19 postPublishWebhookUrl?: string;
20 /** Maximum number of active drafts per user. null = unlimited. */
21 maxDraftsPerUser: number | null;
22 /** When true, createSchedule is rejected with a 403. For demo deployments. */
23 disableRecurring: boolean;
24 /**
25 * Comma-separated ATProto collection NSIDs to allow. Defaults to "*" (all collections).
26 * Used to build the OAuth scope: `repo:<collection>?action=create`.
27 * Example: "app.bsky.feed.post" to restrict to Bluesky posts only.
28 */
29 allowedCollections: string;
30 /** OAuth scope string derived from allowedCollections. Single source of truth. */
31 oauthScope: string;
32}
33
34export const getConfig = (): ServiceConfig => {
35 const databaseType = (process.env.DATABASE_TYPE || 'sqlite') as 'sqlite' | 'postgres';
36
37 const maxDraftsPerUserRaw = process.env.MAX_DRAFTS_PER_USER;
38 const maxDraftsPerUser = maxDraftsPerUserRaw ? parseInt(maxDraftsPerUserRaw, 10) : null;
39
40 const config = {
41 port: parseInt(process.env.ALF_PORT || process.env.PORT || '1986', 10),
42 serviceUrl: process.env.ALF_SERVICE_URL || process.env.SERVICE_URL || 'http://localhost:1986',
43 plcRoot: process.env.PLC_ROOT || 'https://plc.directory',
44 handleResolverUrl: process.env.HANDLE_RESOLVER_URL || process.env.PDS_URL || 'https://api.bsky.app',
45 databaseType,
46 databasePath: process.env.DATABASE_PATH || './data/alf.db',
47 databaseUrl: process.env.DATABASE_URL,
48 encryptionKey: process.env.ENCRYPTION_KEY || '',
49 postPublishWebhookUrl: process.env.POST_PUBLISH_WEBHOOK_URL,
50 maxDraftsPerUser,
51 disableRecurring: ['true', '1', 'yes'].includes((process.env.DISABLE_RECURRING || '').toLowerCase()),
52 allowedCollections: process.env.ALLOWED_COLLECTIONS || '*',
53 oauthScope: `atproto repo:${process.env.ALLOWED_COLLECTIONS || '*'}?action=create blob:*/*`,
54 };
55
56 if (config.databaseType === 'postgres' && !config.databaseUrl) {
57 throw new Error('DATABASE_URL is required when DATABASE_TYPE is "postgres"');
58 }
59 if (!config.encryptionKey) {
60 throw new Error('ENCRYPTION_KEY is required - generate with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
61 }
62 if (!/^[0-9a-fA-F]{64}$/.test(config.encryptionKey)) {
63 throw new Error('ENCRYPTION_KEY must be a 64-character hex string (32 bytes)');
64 }
65
66 return config as ServiceConfig;
67};