[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { Hono } from "hono";
2import { cors } from "hono/cors";
3import { logger } from "hono/logger";
4import { Database } from "./data-plane/db/index.ts";
5import { createAuthVerifier } from "./auth-verifier.ts";
6import API from "./api/index.ts";
7import { createServer } from "./lex/index.ts";
8import wellKnown from "./api/well-known.ts";
9import health from "./api/health.ts";
10import { IdResolver, MemoryCache } from "@atp/identity";
11import { DataPlane } from "./data-plane/index.ts";
12import { Hydrator } from "./hydration/index.ts";
13import { Views } from "./views/index.ts";
14import { AppContext, AppEnv } from "./context.ts";
15import { ServerConfig } from "./config.ts";
16import { defaultLabelerHeader, parseLabelerHeader } from "./util.ts";
17import { PushService } from "./utils/push.ts";
18
19// Create app without starting services
20export function createApp(ctx: AppContext): Hono<AppEnv> {
21 const app = new Hono<AppEnv>();
22
23 app.use("*", cors());
24 app.use("*", logger());
25 app.use("*", async (c, next) => {
26 c.env = ctx;
27 await next();
28 });
29
30 // Lexicon/XRPC server and routers
31 const lexServer = createServer();
32 API(lexServer, ctx);
33
34 app.route("/.well-known", wellKnown);
35 app.route("/", health);
36 app.route("/", lexServer.xrpc.app);
37
38 return app;
39}
40
41// Setup function to create context and app
42export function setupApp(): { app: Hono<AppEnv>; ctx: AppContext } {
43 const cfg = ServerConfig.readEnv();
44 const db = new Database(cfg);
45 db.connect();
46
47 // DID and resolver setup with caching
48 const idResolver = new IdResolver({
49 plcUrl: cfg.plcUrl,
50 didCache: new MemoryCache(),
51 });
52
53 const dataplane = new DataPlane(db, idResolver);
54 const hydrator = new Hydrator(dataplane, cfg.labelsFromIssuerDids);
55 const views = new Views({
56 indexedAtEpoch: cfg.indexedAtEpoch,
57 videoCdn: cfg.videoCdn,
58 mediaCdn: cfg.mediaCdn,
59 thumbCdn: cfg.thumbCdn,
60 });
61
62 const authVerifier = createAuthVerifier(dataplane, {
63 ownDid: cfg.serverDid,
64 alternateAudienceDids: [],
65 modServiceDid: cfg.modServiceDid,
66 adminPasses: cfg.adminPasswords,
67 });
68
69 const reqLabelers = (req: Request) => {
70 const val = req.headers.get("atproto-accept-labelers") ?? undefined;
71 const parsed = parseLabelerHeader(val);
72 if (!parsed) return defaultLabelerHeader(cfg.labelsFromIssuerDids);
73 return parsed;
74 };
75
76 // Create push service for badge management
77 const pushService = new PushService(dataplane.pushTokens, db, {
78 enabled: cfg.pushEnabled,
79 fcmServiceAccount: cfg.fcmServiceAccount,
80 });
81
82 const ctx = {
83 db,
84 dataplane,
85 hydrator,
86 views,
87 idResolver,
88 cfg,
89 authVerifier,
90 reqLabelers,
91 pushService,
92 };
93
94 const app = createApp(ctx);
95 return { app, ctx };
96}
97
98// Start server function
99export function startServer() {
100 const { app, ctx } = setupApp();
101
102 // Start HTTP server immediately
103 const { port } = ctx.cfg;
104 Deno.serve({
105 port,
106 onListen: (info) => {
107 console.info(`Server listening on ${info.hostname}:${info.port}`);
108 },
109 }, app.fetch);
110
111 // Handle shutdown
112 const shutdown = async (signal: string) => {
113 console.info(`Received ${signal}; shutting down...`);
114 try {
115 console.info("Disconnecting database...");
116 await ctx.db.disconnect();
117 } catch (err) {
118 console.error("Error disconnecting database during shutdown", { err });
119 }
120 console.info("Shutdown complete");
121 Deno.exit(0);
122 };
123
124 Deno.addSignalListener("SIGINT", () => shutdown("SIGINT"));
125 Deno.addSignalListener("SIGTERM", () => shutdown("SIGTERM"));
126}
127
128// Start the server if this file is run directly
129if (import.meta.main) {
130 startServer();
131}