···1-# main indexers
2-JETSTREAM_URL="wss://jetstream1.us-east.bsky.network"
3-SPACEDUST_URL="wss://spacedust.whey.party"
4-5-# for backfill (useless if you just started the instance right now)
6-CONSTELLATION_URL="https://constellation.microcosm.blue"
7-# i dont actually know why i need this
8-SLINGSHOT_URL="https://slingshot.whey.party"
9-10-# bools
11-INDEX_SERVER_ENABLED=true
12-INDEX_SERVER_INVITES_REQUIRED=true
13-14-VIEW_SERVER_ENABLED=true
15-INDEX_SERVER_INVITES_REQUIRED=true
16-17-# this is for both index and view server btw
18-SERVICE_DID="did:web:local3768forumtest.whey.party"
19-SERVICE_ENDPOINT="https://local3768forumtest.whey.party"
20-SERVER_PORT="3768"
···1+{
2+ // Main indexers
3+ "jetstream": "wss://jetstream1.us-east.bsky.network", // you can self host it -> https://github.com/bluesky-social/jetstream
4+ "spacedust": "wss://spacedust.your.site", // you can self host it -> https://www.microcosm.blue
5+6+ // For backfill (optional)
7+ "constellation": "https://constellation.microcosm.blue", // (not useful on a new setup — requires pre-existing data to backfill)
8+9+ // Utility services
10+ "slingshot": "https://slingshot.your.site", // you can self host it -> https://www.microcosm.blue
11+12+ // Index Server config
13+ "indexServer": {
14+ "inviteOnly": true,
15+ "port": 3767,
16+ "did": "did:web:skyliteindexserver.your.site", // should be the same domain as the endpoint
17+ "host": "https://skyliteindexserver.your.site"
18+ },
19+20+ // View Server config
21+ "viewServer": {
22+ "inviteOnly": true,
23+ "port": 3768,
24+ "did": "did:web:skyliteviewserver.your.site", // should be the same domain as the endpoint
25+ "host": "https://skyliteviewserver.your.site",
26+27+ // In order of which skylite index servers or bsky appviews to use first
28+ "indexPriority": [
29+ "user#skylite_index", // user resolved skylite index server
30+ "did:web:backupindexserver.your.site#skylite_index", // a specific skylite index server
31+ "user#bsky_appview", // user resolved bsky appview
32+ "did:web:api.bsky.app#bsky_appview" // a specific bsky appview
33+ ]
34+ }
35+}
+45
config.ts
···000000000000000000000000000000000000000000000
···1+import { parse } from "jsr:@std/jsonc";
2+import * as z from "npm:zod";
3+4+// configure these from the config.jsonc file (you can use config.jsonc.example as reference)
5+const indexTarget = z.string().refine(
6+ (val) => {
7+ const parts = val.split("#");
8+ if (parts.length !== 2) return false;
9+10+ const [prefix, suffix] = parts;
11+ const validPrefix = prefix === "user" || prefix.startsWith("did:web:");
12+ const validSuffix = suffix === "skylite_index" || suffix === "bsky_appview";
13+14+ return validPrefix && validSuffix;
15+ },
16+ {
17+ message:
18+ "Each indexPriority entry must be in the form 'user#skylite_index', 'user#bsky_appview', 'did:web:...#skylite_index', or 'did:web:...#bsky_appview'",
19+ }
20+);
21+22+const ConfigSchema = z.object({
23+ jetstream: z.string(),
24+ spacedust: z.string(),
25+ constellation: z.string(),
26+ slingshot: z.string(),
27+ indexServer: z.object({
28+ inviteOnly: z.boolean(),
29+ port: z.number(),
30+ did: z.string(),
31+ host: z.string(),
32+ }),
33+ viewServer: z.object({
34+ inviteOnly: z.boolean(),
35+ port: z.number(),
36+ did: z.string(),
37+ host: z.string(),
38+ indexPriority: z.array(indexTarget),
39+ }),
40+});
41+42+const raw = await Deno.readTextFile("config.jsonc");
43+const config = ConfigSchema.parse(parse(raw));
44+45+export { config };
+2-1
deno.json
···1{
2 "tasks": {
3- "dev": "deno run --watch -A --env-file main.ts"
04 },
5 "imports": {
6 "@std/assert": "jsr:@std/assert@1"
···1{
2 "tasks": {
3+ "index": "deno run --watch -A --env-file main-index.ts",
4+ "view": "deno run --watch -A --env-file main-view.ts"
5 },
6 "imports": {
7 "@std/assert": "jsr:@std/assert@1"
+2-2
index/jetstream.ts
···1import { Database } from "jsr:@db/sqlite@0.11";
2-import { handleIndex } from "../main.ts";
3import { resolveRecordFromURI } from "../utils/records.ts";
4import { JetstreamManager } from "../utils/sharders.ts";
5···29 });
30}
3132-export async function handleJetstream(msg: any) {
33 console.log("Received Jetstream message: ", msg);
3435 const op = msg.commit.operation;
···1import { Database } from "jsr:@db/sqlite@0.11";
2+import { config } from "../config.ts";
3import { resolveRecordFromURI } from "../utils/records.ts";
4import { JetstreamManager } from "../utils/sharders.ts";
5···29 });
30}
3132+export async function handleJetstream(msg: any, handleIndex: Function) {
33 console.log("Received Jetstream message: ", msg);
3435 const op = msg.commit.operation;
···44this project is pre-alpha and not intended for general use yet. you are welcome to experiment if you dont mind errors or breaking changes.
4546the project is split into two, the "Index Server" and the "View Server".
47-these currently run in a single process and share the same HTTP server and port.
4849-configuration is in the `.env` file
5051expose your localhost to the web using a tunnel or something and use that url as the custom appview url
52
···44this project is pre-alpha and not intended for general use yet. you are welcome to experiment if you dont mind errors or breaking changes.
4546the project is split into two, the "Index Server" and the "View Server".
47+despite both living in this repo, they run different http servers with different configs
4849+example configuration is in the `config.jsonc.example` file
5051expose your localhost to the web using a tunnel or something and use that url as the custom appview url
52
···12import { DidResolver, HandleResolver } from "npm:@atproto/identity";
3import { Database } from "jsr:@db/sqlite@0.11";
04const systemDB = new Database("./system.db") // TODO: temporary shim. should seperate this to its own central system db instead of the now instantiated system dbs
5type DidMethod = "web" | "plc";
6type DidDoc = {
···224 } catch (err) {
225 console.error(`Failed to extract/store PDS and handle for '${did}':`, err);
226 return null;
0000000000000227 }
228}
···12import { DidResolver, HandleResolver } from "npm:@atproto/identity";
3import { Database } from "jsr:@db/sqlite@0.11";
4+import { AtUri } from "npm:@atproto/api";
5const systemDB = new Database("./system.db") // TODO: temporary shim. should seperate this to its own central system db instead of the now instantiated system dbs
6type DidMethod = "web" | "plc";
7type DidDoc = {
···225 } catch (err) {
226 console.error(`Failed to extract/store PDS and handle for '${did}':`, err);
227 return null;
228+ }
229+}
230+231+export function extractDid(input: string): string {
232+ if (input.startsWith('did:')) {
233+ return input
234+ }
235+236+ try {
237+ const uri = new AtUri(input)
238+ return uri.host
239+ } catch (e) {
240+ throw new Error(`Invalid input: expected a DID or a valid AT URI, got "${input}"`)
241 }
242}
+4-4
utils/server.ts
···1import ky from "npm:ky";
2import QuickLRU from "npm:quick-lru";
3import { createHash } from "node:crypto";
4-import { slingshoturl, constellationurl } from "../main.ts";
5import * as ATPAPI from "npm:@atproto/api";
67const cache = new QuickLRU({ maxSize: 10000 });
···57export async function resolveIdentity(
58 actor: string
59): Promise<SlingshotMiniDoc> {
60- const url = `${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`;
61 return (await cachedFetch(url)) as SlingshotMiniDoc;
62}
63export async function getRecord({
···155 collection: string,
156 rkey: string
157): Promise<GetRecord> {
158- const url = `${slingshoturl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`;
159 const result = (await cachedFetch(url)) as GetRecord;
160 return result as GetRecord;
161}
···169 collection: string;
170 path: string;
171}): Promise<number> {
172- const url = `${constellationurl}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`;
173 const result = (await cachedFetch(url)) as ConstellationDistinctDids;
174 return result.total;
175}
···1import ky from "npm:ky";
2import QuickLRU from "npm:quick-lru";
3import { createHash } from "node:crypto";
4+import { config } from "../config.ts";
5import * as ATPAPI from "npm:@atproto/api";
67const cache = new QuickLRU({ maxSize: 10000 });
···57export async function resolveIdentity(
58 actor: string
59): Promise<SlingshotMiniDoc> {
60+ const url = `${config.slingshot}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`;
61 return (await cachedFetch(url)) as SlingshotMiniDoc;
62}
63export async function getRecord({
···155 collection: string,
156 rkey: string
157): Promise<GetRecord> {
158+ const url = `${config.slingshot}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`;
159 const result = (await cachedFetch(url)) as GetRecord;
160 return result as GetRecord;
161}
···169 collection: string;
170 path: string;
171}): Promise<number> {
172+ const url = `${config.constellation}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`;
173 const result = (await cachedFetch(url)) as ConstellationDistinctDids;
174 return result.total;
175}