···11+{
22+ "lexicon": 1,
33+ "id": "app.wafrn.graph.getRelationship",
44+ "defs": {
55+ "main": {
66+ "type": "query",
77+ "description": "Get the follow relationship between two users (bidirectional)",
88+ "parameters": {
99+ "type": "params",
1010+ "required": ["actor", "viewer"],
1111+ "properties": {
1212+ "actor": {
1313+ "type": "string",
1414+ "format": "did",
1515+ "description": "DID of the actor (typically the profile being viewed)"
1616+ },
1717+ "viewer": {
1818+ "type": "string",
1919+ "format": "did",
2020+ "description": "DID of the viewer (typically the current user)"
2121+ }
2222+ }
2323+ },
2424+ "output": {
2525+ "encoding": "application/json",
2626+ "schema": {
2727+ "type": "object",
2828+ "required": ["following", "followedBy"],
2929+ "properties": {
3030+ "following": {
3131+ "type": "boolean",
3232+ "description": "True if viewer follows actor"
3333+ },
3434+ "followedBy": {
3535+ "type": "boolean",
3636+ "description": "True if actor follows viewer"
3737+ }
3838+ }
3939+ }
4040+ }
4141+ }
4242+ }
4343+}
+1
packages/lexicon/index.ts
···1111export * as AppWafrnGraphFollow from "./types/app/wafrn/graph/follow.js";
1212export * as AppWafrnGraphGetFollowers from "./types/app/wafrn/graph/getFollowers.js";
1313export * as AppWafrnGraphGetFollows from "./types/app/wafrn/graph/getFollows.js";
1414+export * as AppWafrnGraphGetRelationship from "./types/app/wafrn/graph/getRelationship.js";
1415export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js";
···11+import type {} from "@atcute/lexicons";
22+import * as v from "@atcute/lexicons/validations";
33+import type {} from "@atcute/lexicons/ambient";
44+55+const _mainSchema = /*#__PURE__*/ v.query("app.wafrn.graph.getRelationship", {
66+ params: /*#__PURE__*/ v.object({
77+ /**
88+ * DID of the actor (typically the profile being viewed)
99+ */
1010+ actor: /*#__PURE__*/ v.didString(),
1111+ /**
1212+ * DID of the viewer (typically the current user)
1313+ */
1414+ viewer: /*#__PURE__*/ v.didString(),
1515+ }),
1616+ output: {
1717+ type: "lex",
1818+ schema: /*#__PURE__*/ v.object({
1919+ /**
2020+ * True if actor follows viewer
2121+ */
2222+ followedBy: /*#__PURE__*/ v.boolean(),
2323+ /**
2424+ * True if viewer follows actor
2525+ */
2626+ following: /*#__PURE__*/ v.boolean(),
2727+ }),
2828+ },
2929+});
3030+3131+type main$schematype = typeof _mainSchema;
3232+3333+export interface mainSchema extends main$schematype {}
3434+3535+export const mainSchema = _mainSchema as mainSchema;
3636+3737+export interface $params extends v.InferInput<mainSchema["params"]> {}
3838+export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {}
3939+4040+declare module "@atcute/lexicons/ambient" {
4141+ interface XRPCQueries {
4242+ "app.wafrn.graph.getRelationship": mainSchema;
4343+ }
4444+}
+41
packages/server/CLAUDE.md
···228228229229**Design rationale**: Post feeds are the hottest query path in the application. By making counts optional and defaulting to `false`, we optimize the most common case while supporting full profile views when needed.
230230231231+**Checking User Relationships**:
232232+233233+The `app.wafrn.graph.getRelationship` endpoint checks the bidirectional follow relationship between two users:
234234+235235+```typescript
236236+// Check relationship between viewer and profile
237237+const relationship = await serverClient.get('app.wafrn.graph.getRelationship', {
238238+ params: {
239239+ actor: profileUserDid, // Profile being viewed
240240+ viewer: currentUserDid // Current user viewing
241241+ }
242242+})
243243+244244+// Display appropriate UI based on relationship
245245+if (relationship.following && relationship.followedBy) {
246246+ // Mutual follows - show "Following" with mutual badge
247247+ return <button className="btn">Following · Mutual</button>
248248+} else if (relationship.following) {
249249+ // You follow them - show "Following"
250250+ return <button className="btn">Following</button>
251251+} else if (relationship.followedBy) {
252252+ // They follow you - show "Follow Back"
253253+ return <button className="btn btn-primary">Follow Back</button>
254254+} else {
255255+ // No relationship - show "Follow"
256256+ return <button className="btn btn-primary">Follow</button>
257257+}
258258+```
259259+260260+**Response fields**:
261261+- `following` - True if viewer follows actor
262262+- `followedBy` - True if actor follows viewer
263263+264264+**Common UI patterns**:
265265+- No relationship: "Follow" button
266266+- Viewer follows actor: "Following" button
267267+- Actor follows viewer: "Follow Back" button (encourages reciprocation)
268268+- Mutual follows: "Following" button with "Mutual" badge
269269+270270+**Performance**: Uses `Promise.all` to check both directions in parallel (efficient).
271271+231272### Path Aliases
232273233274The server uses the `@api/*` path alias for clean internal imports:
+18-1
packages/server/src/lib/xrpcServer.ts
···99 AppWafrnGraphDeleteFollow,
1010 AppWafrnGraphGetFollowers,
1111 AppWafrnGraphGetFollows,
1212+ AppWafrnGraphGetRelationship,
1213 AppWafrnActorGetProfiles
1314} from '@watproto/lexicon'
1415import { didDocResolver } from './idResolver'
···2021 deleteFollow,
2122 getFollowCountsBatch,
2223 getFollowers,
2323- getFollowing
2424+ getFollowing,
2525+ isFollowing
2426} from '@api/lib/follow'
2527import { getWafrnProfiles } from '@api/lib/profile'
2628···321323 createdAt: new Date(f.created_at).toISOString(),
322324 uri: f.uri as ResourceUri
323325 }))
326326+ })
327327+ }
328328+})
329329+330330+xrpcServer.addQuery(AppWafrnGraphGetRelationship.mainSchema, {
331331+ async handler({ params }) {
332332+ // Check both directions in parallel
333333+ const [following, followedBy] = await Promise.all([
334334+ isFollowing(params.viewer, params.actor), // viewer -> actor
335335+ isFollowing(params.actor, params.viewer) // actor -> viewer
336336+ ])
337337+338338+ return json({
339339+ following, // Does viewer follow actor?
340340+ followedBy // Does actor follow viewer?
324341 })
325342 }
326343})