Skip to content

Add actors table and actor GraphQL API - #47

Merged
2chanhaeng merged 15 commits into
fedify-dev:mainfrom
2chanhaeng:create-actor
Sep 6, 2026
Merged

Add actors table and actor GraphQL API#47
2chanhaeng merged 15 commits into
fedify-dev:mainfrom
2chanhaeng:create-actor

Conversation

@2chanhaeng

@2chanhaeng 2chanhaeng commented Aug 4, 2026

Copy link
Copy Markdown
Member

Resolves #6.
Add actors table and actor GraphQL API to create and reads.

To merge this PR, #44 must be preceded.

Before starting the server, please remove the existing .pgdata/ to clean up the DB.

Assisted-by: Codex:gpt-5-6-sol to generate test and implement merging remoteActors into actors.
Assisted-by: Claude Code:claude-fable-5 to plan merging remoteActors into actors.

@2chanhaeng
2chanhaeng requested review from dahlia, dodok8 and sij411 August 4, 2026 05:14
@2chanhaeng 2chanhaeng self-assigned this Aug 4, 2026
dodok8
dodok8 previously approved these changes Aug 13, 2026
@sij411

sij411 commented Aug 14, 2026

Copy link
Copy Markdown
Member

I have some comments related to actor schema. I see only followee/follower counters not actual follow relationship among actors. I don't think remoteActors.followeeUrl is the right method to fetch remote actor's following/followers. It's 'remote' that means their server status is outside our controls. So we should think about storing follow relationship i think. Also as far as i know, 'following' is more common than the word 'followee' in other ActivityPub implementations.

@2chanhaeng

Copy link
Copy Markdown
Member Author

I have some comments related to actor schema. I see only followee/follower counters not actual follow relationship among actors. I don't think remoteActors.followeeUrl is the right method to fetch remote actor's following/followers. It's 'remote' that means their server status is outside our controls. So we should think about storing follow relationship i think. Also as far as i know, 'following' is more common than the word 'followee' in other ActivityPub implementations.

I haven't fully implemented the follow-related features yet since they weren't part of the current goal. I've kept the terminology as consistent as possible with the Hackers' Pub codebase. Personally, I also prefer the term "followee" because it provides a clear contrast to "follower."

Comment thread packages/graphql/src/actor.ts Outdated
@2chanhaeng
2chanhaeng requested review from dodok8 and sij411 August 21, 2026 08:28
Comment thread packages/graphql/src/actor.test.ts
Comment thread packages/graphql/src/instance.ts Outdated
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Aug 29, 2026
Comment thread packages/graphql/src/actor.ts Outdated
Comment thread packages/graphql/src/actor.ts Outdated
Comment thread packages/graphql/src/uri-templates.ts Outdated
@dahlia dahlia added this to the DrFed 0.1.0 milestone Sep 4, 2026
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
Apply the fixes from an AI-assisted code review of the create-actor
branch (fedify-dev#47).

 -  Mount the Fedify federation in front of the Yoga server.  The srvx
    fetch handler now calls federation.fetch() and falls back to Yoga
    via onNotFound and onNotAcceptable, so the actor, WebFinger, inbox,
    and collection dispatchers are reachable over HTTP instead of only
    serving as URI builders.
 -  Close the database client (PGlite or postgres.js) during shutdown
    before exiting the process.
 -  Serialize genActors per instance by locking the local_instances row
    with SELECT ... FOR UPDATE before counting actors.  This closes the
    race where two concurrent calls both passed the maxActors check
    under READ COMMITTED.  The post-insert recount, tx.rollback(), the
    outer mutable state, and the string-matched catch block are gone;
    every error path now returns a result object directly from the
    transaction callback.
 -  Derive the instance host from local_instances.slug and ctx.root
    rather than reading instances.host.
 -  Generate one UUID per actor and reuse it for local_actors.id,
    actors.id, actors.localId, and the placeholder username, dropping
    the .returning() round trip on the local_actors insert.
 -  Push the local-actor and host predicates into the Drizzle relational
    where clause in findLocalActor and mapHandle, replacing the full-row
    load plus JS filtering and the two-step instance/actor lookup.
 -  Import hashSecret from auth/hash.ts in actor.test.ts and
    instance.test.ts instead of keeping duplicate copies.

Provenance: the contributor asked Claude Code to review the branch
against the pull request comments and the overall diff.  Claude Code
produced the review findings (unmounted federation routes, the
maxActors race, the unclosed database client, the duplicated lookups,
and the duplicated test helper) and drafted this commit message.  The
contributor implemented every change in this commit by hand and
verified them with mise run check and the package test suites.

Assisted-by: Claude Code:claude-fable-5-1
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
Apply the fixes from an AI-assisted code review of the create-actor
branch (fedify-dev#47).

 -  Mount the Fedify federation in front of the Yoga server.  The srvx
    fetch handler now calls federation.fetch() and falls back to Yoga
    via onNotFound and onNotAcceptable, so the actor, WebFinger, inbox,
    and collection dispatchers are reachable over HTTP instead of only
    serving as URI builders.
 -  Close the database client (PGlite or postgres.js) during shutdown
    before exiting the process.
 -  Serialize genActors per instance by locking the local_instances row
    with SELECT ... FOR UPDATE before counting actors.  This closes the
    race where two concurrent calls both passed the maxActors check
    under READ COMMITTED.  The post-insert recount, tx.rollback(), the
    outer mutable state, and the string-matched catch block are gone;
    every error path now returns a result object directly from the
    transaction callback.
 -  Derive the instance host from local_instances.slug and ctx.root
    rather than reading instances.host.
 -  Generate one UUID per actor and reuse it for local_actors.id,
    actors.id, actors.localId, and the placeholder username, dropping
    the .returning() round trip on the local_actors insert.
 -  Push the local-actor and host predicates into the Drizzle relational
    where clause in findLocalActor and mapHandle, replacing the full-row
    load plus JS filtering and the two-step instance/actor lookup.
 -  Import hashSecret from auth/hash.ts in actor.test.ts and
    instance.test.ts instead of keeping duplicate copies.

Provenance: the contributor asked Claude Code to review the branch
against the pull request comments and the overall diff.  Claude Code
produced the review findings (unmounted federation routes, the
maxActors race, the unclosed database client, the duplicated lookups,
and the duplicated test helper) and drafted this commit message.  The
contributor implemented every change in this commit by hand and
verified them with mise run check and the package test suites.

Assisted-by: Claude Code:claude-fable-5-1
@sij411

sij411 commented Sep 5, 2026

Copy link
Copy Markdown
Member

Conflict should be resolved first

Apply the same shape to actors that 09be000 applied to instances:

 -  Move the remote_actors columns (iri, inboxUrl, outboxUrl, and the
    optional followers/followees/featured/profile/avatar/header URLs)
    into actors and drop the remote_actors table.  iriUrl is renamed to
    iri and stays unique.
 -  Drop the location enum column and the (id, location) composite
    FK/CHECK pairs.  Local actors are identified by actors.localId,
    a nullable unique FK to local_actors.id with cascade delete, which
    inverts the previous local_actors.id -> actors.id dependency.
    local_actors keeps only id, avatar, and header.
 -  Replace the actor migration.  The old 20260803204536 migration
    created actors with the "location" type that the later 20260818
    migration drops, so it could not apply on a fresh database and it
    was never on main.  The new migration is regenerated from the
    current snapshot.
 -  Store local actor URLs at creation time.  genActors inserts
    local_actors first, then actors with the URLs expanded from the
    URI templates using instances.host, and joins local_instances via
    instances.localId.  avatarUrl and headerUrl start as NULL.
 -  Simplify the Actor GraphQL type to expose stored columns, keep
    handle as a field-level select on instance.host, and add uuid,
    created, and a nullable local: LocalActor relation.  Replace the
    CreateActors node with Actor in CreateActorsSuccess.actors.
 -  Fix the Instance.actors connection, whose resolveNode returned the
    instanceId instead of the actor.
 -  Update the actor tests for the new seeds, fields, and assertions.

AI provenance: Claude Code was asked to analyze the instance merge in
09be000 and write a plan applying the same approach to actors, covering
the schema, migration, GraphQL API, and tests; it found that the branch
no longer type-checked after the rebase and that the old actor migration
depended on a dropped enum type.  Codex implemented the changes from
that plan.  Claude Code then reviewed the diff against the plan, ran
tsc for each package, and ran the test suite.  The human contributor
has read and reviewed both the plan and the implementation, and directly
modified some of the designs. They validated the generated migration SQL
and performed local checks and tests.

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.6-sol
Replace the ad-hoc URI templates in the GraphQL package with Fedify's
Federation object as the single source of truth for ActivityPub URIs.

federation.ts now registers the actor, inbox, outbox, followers,
following, and featured dispatchers, deriving each URL from the
`Context` getters instead of string templates. The actor dispatcher
scopes lookups to the requesting host so a multi-tenant DrFed instance
never leaks one instance's actors under another's host, maps deleted
actors to `Tombstone` objects, and resolves WebFinger handles via
`mapHandle`. The inbox listener registers a catch-all logger so
incoming activities are visible instead of silently discarded, since
the data model has no follows/posts tables yet to persist them.

actor.ts's `genActor` and the `handle` field now go through a Fedify
`Context` created from `ServerContext.federation` instead of the old
`uri-templates.ts` module, which is removed along with the
`@fedify/uri-template` based templates.

Adds `@fedify/vocab` as a direct dependency for the ActivityPub
vocabulary classes used to build actor responses.

Provenance: the contributor asked Claude Code to implement
`buildFederation` in federation.ts using Fedify dispatcher setters
(actor, inbox, outbox, followers, following, featured) and to replace
the `uri-templates.ts`-based URI generation in actor.ts with Fedify's
`Context` URI getters, then remove uri-templates.ts. Claude Code
inspected the installed Fedify/vocab type declarations and the
project's Drizzle schema to draft the dispatcher implementations,
including the tombstone handling for deleted actors, the per-host
actor scoping, and the inbox catch-all logger, refined after a
follow-up automated review of the draft flagged missing inbox
visibility and 404-vs-Tombstone handling for deleted actors. The
contributor then asked Claude Code to hoist `findLocalActor` out of
`buildFederation` into a module-level function that takes the
database as an explicit argument, which was applied directly. The
contributor verified the result by running `mise run check`, the
package's `tsc --noEmit`, its test suite, and by exercising the
dispatchers against a temporary PGlite-backed Federation instance
(actor lookup, tombstone response for deleted actors, followers
collection, and cross-host isolation).

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Claude Code:claude-sonnet-5
PostgreSQL access was split between two drivers: Drizzle used pg
(node-postgres) while Fedify's PostgresKvStore used postgres
(postgres-js), so a --postgres-url server opened two separate
connection pools.  Unify on postgres:

- @drfed/models: migrate() now uses drizzle-orm/postgres-js.  The
  PostgreSQL credentials mirror the PGlite shape: either a url with
  optional postgres.Options (a max: 1 client is created and closed
  after migrating) or an existing Sql client that is left open.
- @drfed/drfed: the --postgres-url parser creates one postgres()
  client and shares it between Drizzle and PostgresKvStore through
  credentials.client.
- Drop pg and @types/pg from both packages and the catalog; move
  postgres into the catalog.
- Prune the orphaned pg-related entries from pnpm-lock.yaml, which
  pnpm did not remove on its own because drizzle-orm still lists pg
  as an optional peer.

drizzle-orm/pg-core imports are the Drizzle dialect module, not the
pg package, and remain.

Verified with mise run check/build/test, a PGlite startup, and a
PostgreSQL 17 container (migrations applied, GraphQL responding).

AI assistance: Claude Code drafted the driver replacement, the
credential type changes, the lockfile pruning, and this message; the
result was reviewed and verified by a human.

Assisted-by: Claude Code:claude-fable-5-1
createYogaServer() used to register the ActivityPub dispatchers on the
Federation instance it received, so calling it twice with the same
instance crashed with a RouterError from Fedify's duplicate-route check.
The constraint was not documented on the option either.

Move the registration into the Federation factory itself:

- packages/graphql/src/federation.ts now exports buildFederation(), which
  registers every dispatcher and listener on a fresh Fedify
  FederationBuilder, and a default createFederation() that builds the
  Federation from that builder with the given options.
- createYogaServer() takes the built Federation as its second positional
  argument and only stores it in the resolver context.  The federation
  field is removed from YogaServerOptions, which is optional again.
- @drfed/graphql exposes the ./federation subpath so that @drfed/drfed
  can build the Federation at startup.
- The test harness builds a Federation with MemoryKvStore and exposes it;
  federation.test.ts checks the URI layout, that one builder yields
  independent instances, and that createYogaServer() no longer mutates
  the instance it is given.

This commit was produced with AI assistance.  The approach (moving the
dispatcher registration to the Federation factory and using Fedify's
FederationBuilder) and the second-positional-argument signature were
chosen by the human author; the assistant wrote the code, tests, and
documentation updates, and verified them with the mise check, build,
and test tasks and a manual server start.

Assisted-by: Claude Code:claude-fable-5-1
Apply the fixes from an AI-assisted code review of the create-actor
branch (fedify-dev#47).

 -  Mount the Fedify federation in front of the Yoga server.  The srvx
    fetch handler now calls federation.fetch() and falls back to Yoga
    via onNotFound and onNotAcceptable, so the actor, WebFinger, inbox,
    and collection dispatchers are reachable over HTTP instead of only
    serving as URI builders.
 -  Close the database client (PGlite or postgres.js) during shutdown
    before exiting the process.
 -  Serialize genActors per instance by locking the local_instances row
    with SELECT ... FOR UPDATE before counting actors.  This closes the
    race where two concurrent calls both passed the maxActors check
    under READ COMMITTED.  The post-insert recount, tx.rollback(), the
    outer mutable state, and the string-matched catch block are gone;
    every error path now returns a result object directly from the
    transaction callback.
 -  Derive the instance host from local_instances.slug and ctx.root
    rather than reading instances.host.
 -  Generate one UUID per actor and reuse it for local_actors.id,
    actors.id, actors.localId, and the placeholder username, dropping
    the .returning() round trip on the local_actors insert.
 -  Push the local-actor and host predicates into the Drizzle relational
    where clause in findLocalActor and mapHandle, replacing the full-row
    load plus JS filtering and the two-step instance/actor lookup.
 -  Import hashSecret from auth/hash.ts in actor.test.ts and
    instance.test.ts instead of keeping duplicate copies.

Provenance: the contributor asked Claude Code to review the branch
against the pull request comments and the overall diff.  Claude Code
produced the review findings (unmounted federation routes, the
maxActors race, the unclosed database client, the duplicated lookups,
and the duplicated test helper) and drafted this commit message.  The
contributor implemented every change in this commit by hand and
verified them with mise run check and the package test suites.

Assisted-by: Claude Code:claude-fable-5-1
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 5, 2026
Code review objected to abbreviated identifiers.  Rename the
`Mutation.genActors` field to `Mutation.generateActors` and update
the test's operation name, query constant, and assertions to match.

The rename was applied by a subagent and reviewed by the assistant,
which also caught the remaining `GenActors` operation name in the test.

fedify-dev#47 (comment)

Assisted-by: Claude Code:claude-fable-5-1
@2chanhaeng

Copy link
Copy Markdown
Member Author

Conflict should be resolved first

Resolved!

dahlia
dahlia previously approved these changes Sep 5, 2026
@dahlia dahlia added the enhancement New feature or request label Sep 5, 2026
@dahlia dahlia moved this from Todo to In progress in NLnet NGI0 Commons Fund (2026) Sep 5, 2026
Comment thread packages/graphql/src/actor.ts Outdated
Code review objected to abbreviated identifiers.  Rename the
`Mutation.genActors` field to `Mutation.generateActors`, the `genActor`
helper to `generateActor`, and update the test's operation name, query
constant, and assertions to match.

This is a breaking change to the GraphQL schema.  No frontend code
referenced the old field name.

The rename was applied by a subagent and reviewed by the assistant,
which also caught the remaining `GenActors` operation name in the test.

Assisted-by: Claude Code:claude-fable-5-1
Comment thread packages/graphql/src/federation.ts
Rename actors.followeesUrl/followeesCount columns to followingUrl and
followingCount, and update the GraphQL field and Fedify dispatcher path
to match. A new migration renames the columns in place.

Assisted-by: Claude Code:claude-fable-5-1
@2chanhaeng
2chanhaeng requested a review from sij411 September 6, 2026 05:47
@2chanhaeng
2chanhaeng merged commit 9d19506 into fedify-dev:main Sep 6, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from In progress to Done in NLnet NGI0 Commons Fund (2026) Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Development

Successfully merging this pull request may close these issues.

GraphQL API for creating actors

4 participants