LinkedAt Agent Guide #
Project Snapshot #
LinkedAt is a pnpm/Turbo TypeScript workspace for an ATProto professional-networking MVP. The product intentionally reuses existing public records wherever possible:
- Sifa
id.sifa.profile.*records for profiles, positions, education, and skills. - XPTracker
app.xptracker.job.listingrecords for MVP job listings. - LinkedAt-owned
app.linkedat.profile.resumeonly as a resume metadata sidecar.
ATProto repository records and blobs are public. UI copy and implementation decisions must not imply that uploaded resumes, imported JSON Resume data, or profile/job records are private.
Workspace Layout #
apps/web: React Router v7 Framework Mode app, React UI, route loaders/actions, public pages, the in-memory index and its stand-ins underapp/data/, Node test-runner tests discovered recursively throughscripts/run-tests.mjs, and Playwright e2e tests.apps/indexer: Jetstream/backfill worker and record normalization handlers.packages/atproto: OAuth, identity resolution, record read/write adapters, and schema.org projection.packages/db: Postgres client, schema, repositories, and migration script.packages/lexicons: pinned external lexicons, LinkedAt sidecar lexicons, validation scripts, and lexicon tests.docs: protocol research, namespace policy, and schema mapping decisions.
Commands #
Use pnpm, not npm or yarn. This repo expects Node >=24 and pnpm >=11.
- Install dependencies:
pnpm install - Start the web app:
pnpm dev - Start the indexer:
pnpm indexer:dev - Build all workspaces:
pnpm build - Type check:
pnpm typecheck - Lint:
pnpm lint(ESLint, type-aware) - Format:
pnpm format/ check withpnpm format:check(Prettier) - Run all unit tests:
pnpm test - Run web e2e tests:
pnpm --filter @linkedat/web test:e2e - Run database migrations:
pnpm db:migrate - Validate lexicons:
pnpm --filter @linkedat/lexicons validate - Refresh pinned external lexicons when they are missing or intentionally stale:
pnpm --filter @linkedat/lexicons fetch:external
For narrow changes, prefer the relevant filtered package command first, then run the broader command when the change crosses package boundaries.
What Is Real #
Read the Status table in README.md before assuming a feature works. Most of this app is not wired to anything yet, and the parts that are faked are confined to three files:
apps/web/app/data/repositories.server.tsstands in for the index.apps/web/app/data/record-writer.server.tsstands in for a PDS.apps/web/app/data/dev-actor.server.tsstands in for the signed-in user.
Two rules follow from that:
- Do not build a feature that only appears to work. If a form cannot publish, disable it and say why in the UI, the way the profile, experience, and resume forms do. An action that validates input and discards the result is worse than an obvious gap, because it invites the next person to build on nothing.
- Do not widen the fakes. New functionality either runs the real code path against a stand-in, the way job posting does, or it is explicitly not implemented. Adding a fourth stand-in needs a reason.
Testing Rules #
- A test must be able to fail for a reason a user would care about. Do not assert that a source file contains a string, that a stylesheet contains a hex value, or that an import exists. Those tests only fail when someone edits the file they read. This suite had 26 of them; they were deleted, not replaced.
- Prove new tests can fail. Break the thing on purpose, watch the test go red, then put it back. This is cheap and it is the only evidence that a green suite means anything.
- Visual and layout behaviour belongs in Playwright, against a real browser.
- Tests that share the app's in-memory index (
job-publish-flow.test.ts) must use distinctive data and must never assert on totals.
Development Conventions #
- TypeScript is strict, with
noUncheckedIndexedAccessandexactOptionalPropertyTypesenabled. - Shared package imports should use workspace package names such as
@linkedat/atproto,@linkedat/db, and@linkedat/lexicons. - Keep package responsibilities separated: web UI and server actions in
apps/web, protocol adapters inpackages/atproto, persistence inpackages/db, and lexicon validation/data inpackages/lexicons. - Tests use Node's built-in test runner for package/unit tests. The web app runs tests through
apps/web/scripts/run-tests.mjs, which invokesnode --import tsx --testwith explicit.test.tsand.test.tsxfiles discovered underapps/web/tests. Web e2e tests use Playwright. - Do not add secrets to the repo. Copy
.env.exampleto.envlocally for real OAuth, database, or indexer runs.
Protocol Rules #
- Do not create a primary
app.linkedat.job.*lexicon for the MVP. - Do not create LinkedAt-owned replacements for Sifa profile, position, education, or skill records.
- LinkedAt job creation must satisfy XPTracker's required fields:
title,company,sourceUri, andcreatedAt. - If richer job semantics are needed later, first pursue compatible XPTracker extensions. A LinkedAt-owned job namespace is a future decision point only.
- Keep external lexicons pinned under
packages/lexicons/external. Refresh them intentionally withpnpm --filter @linkedat/lexicons fetch:externalrather than ad hoc edits, especially before validation when pinned files are missing or stale. - Schema.org JSON-LD should only project truthful data present in indexed Sifa or XPTracker records. Omit unavailable fields instead of inventing values.
Verification Expectations #
Before handing off code changes, run the smallest meaningful verification command and report exactly what ran. Typical defaults:
- Shared/package logic:
pnpm typecheck,pnpm lint, andpnpm test - Web rendering or server-action changes:
pnpm --filter @linkedat/web testpluspnpm typecheck - Browser workflow changes:
pnpm --filter @linkedat/web test:e2e - Lexicon changes:
pnpm --filter @linkedat/lexicons validateandpnpm --filter @linkedat/lexicons test
If a command cannot run because local services or environment variables are missing, state that explicitly and include the next best verification performed. Never report work as done without running something; "typecheck passes" is not evidence that a feature works.