# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Guidelines

- Never run the project directly (pnpm `dev` or `start`). I'll run any build or serve operations manually through `make` targets in a separate terminal.
- Use `pnpm exec` instead of `npx` to run local binaries (e.g. `pnpm exec vitest run ...`).
- When listing a `make` target always specify all variables that need to be set like the domain or the AWS profile.
- By default, the AWS profile names for local development and AWS are `localstack` and `production`, respectively.
- `DOMAIN_NAME` is not needed for local `make` targets (only for production targets).
- Never suggest actions that cause CloudFormation stack drift (e.g. manually terminating EC2 instances, deleting resources outside of CloudFormation). Always go through CloudFormation to make infrastructure changes.

## Important reference material

- AT Protocol https://atproto.com/docs
- Advanced Guides https://docs.bsky.app/docs/category/advanced-guides
- Lexicon tooling 
    - https://github.com/bluesky-social/atproto/tree/main/packages/lex/lex
    - https://github.com/bluesky-social/atproto/tree/main/packages/lex-cli
- Publications
    - Lexicon: https://pdsls.dev/at://did:plc:re3ebnp5v7ffagz6rb6xfei4/com.atproto.lexicon.schema/site.standard.publication  
    - Docs: https://standard.site/docs/lexicons/publication/
- Documents
    - Lexicon https://pdsls.dev/at://did:plc:re3ebnp5v7ffagz6rb6xfei4/com.atproto.lexicon.schema/site.standard.document  
    - Docs: https://standard.site/docs/lexicons/document/

- Subscriptions
    - Lexicon: https://pdsls.dev/at://did:plc:re3ebnp5v7ffagz6rb6xfei4/com.atproto.lexicon.schema/site.standard.graph.subscription  
    - Docs: https://standard.site/docs/lexicons/subscription/


## Commands

```bash
pnpm build        # Production build (outputs to build/client/ and build/server/)
pnpm typecheck    # Regenerate React Router types then run tsc
```

Run unit tests from the package directory (e.g. `infra/api/`):

```bash
pnpm exec vitest run          # Run all tests once
pnpm exec vitest run <file>   # Run a specific test file
```

## API Gateway resources

Every new API route requires **two** changes:
1. Handler logic in `infra/api/src/index.ts`
2. Explicit resource + method definitions in `infra/cf/blog-api.yaml` — the API Gateway does **not** use a catch-all proxy. Each path needs its own `AWS::ApiGateway::Resource` and one `AWS::ApiGateway::Method` per HTTP verb, plus an OPTIONS method with a MOCK integration for CORS. The new method names must also be added to the `DependsOn` list of `BlogApiDeployment`. Without this, the preflight OPTIONS request never reaches the Lambda and CORS fails.

Use `create-local-api` (not `update-local-api`) whenever `blog-api.yaml` changes.

## Deploying updates

| What changed | Local | Production |
|---|---|---|
| `app/` (frontend) | `AWS_PROFILE=localstack make -C infra update-local-site` | `AWS_PROFILE=production DOMAIN_NAME=lemma.pub make -C infra deploy-site` |
| `infra/api/` (Lambda code only) | `AWS_PROFILE=localstack make -C infra update-local-api` | `AWS_PROFILE=production make -C infra update-aws-api` |
| `infra/api/` (CloudFormation changes) | `AWS_PROFILE=localstack make -C infra create-local-api` | `AWS_PROFILE=production DOMAIN_NAME=lemma.pub make -C infra create-aws-api` |
| `infra/api/` (both code + CloudFormation) | `AWS_PROFILE=localstack make -C infra create-local-api` | `AWS_PROFILE=production DOMAIN_NAME=lemma.pub make -C infra create-aws-api` and `AWS_PROFILE=production make -C infra update-aws-api` |
| Both frontend + API | `AWS_PROFILE=localstack make -C infra update-local` | `AWS_PROFILE=production DOMAIN_NAME=lemma.pub make -C infra deploy-site` and `AWS_PROFILE=production make -C infra update-aws-api` |
| `infra/indexer/` (code only) | `AWS_PROFILE=localstack make -C infra run-local-indexer` | `AWS_PROFILE=production make -C infra update-aws-indexer` |
| `infra/indexer/` (CloudFormation changes) | `AWS_PROFILE=localstack make -C infra deploy-local-indexer` | `AWS_PROFILE=production make -C infra deploy-aws-indexer` |
| Restart indexer (no code change) | — | `AWS_PROFILE=production make -C infra restart-aws-indexer` |
| `infra/backfill/` (Lambda code only) | `AWS_PROFILE=localstack make -C infra update-local-backfill` | `AWS_PROFILE=production make -C infra update-aws-backfill` |
| `infra/backfill/` (CloudFormation changes) | `AWS_PROFILE=localstack make -C infra deploy-local-backfill` | `AWS_PROFILE=production make -C infra deploy-aws-backfill` |
| Run backfill | `AWS_PROFILE=localstack make -C infra run-local-backfill DIDS_FILE=~/Drive/dids.txt` | `AWS_PROFILE=production make -C infra run-aws-backfill DIDS_FILE=~/Drive/dids.txt` |

`DOMAIN_NAME=lemma.pub` is required for `deploy-site` and `create-aws-api` (sets `API_DID=did:web:lemma.pub` on the Lambda — omitting it resets `API_DID` to `did:web:localhost` and breaks all authenticated endpoints). Lambda-zip-only and indexer targets do not use it.

`create-aws-api` only updates the CloudFormation stack (API Gateway resources). It does **not** deploy the Lambda zip. When `infra/api/src/` and `blog-api.yaml` both change, you must run both `create-aws-api` and `update-aws-api`. Skipping `update-aws-api` leaves the old Lambda code running, which returns 405 for any new routes even after the API Gateway resources are created.

The indexer is a separate long-running process — `update-local` does **not** cover it. Restart it separately whenever `infra/indexer/` changes.

## Docs

Architectural decisions and infrastructure details live in `docs/`:

- `docs/architecture.md` — end-to-end flow, infrastructure, DynamoDB schema, following feed, indexer, backfill Lambda, front end, and S3/deployment.
- `docs/atproto-auth.md` — ATProto OAuth: client metadata, handle resolution, session lifecycle, DPoP.
- `docs/api-auth.md` — API authentication flow, service JWT verification, API Lambda deployment.
- `docs/security.md` — security design decisions, mitigations, and known trade-offs.
