# External Integrations **Analysis Date:** 2026-05-11 ## APIs & External Services **AI Providers (user-supplied API keys, called per-request from `packages/api/src/services/ai.ts`):** - OpenAI — Default base URL `https://api.openai.com/v1` (`packages/ai/src/types.ts`) - SDK: `@ai-sdk/openai ^3.0.63` via `createOpenAI(...).chat(model)` - API key supplied per-call from the resume's `ai` config; not read from server env - Anthropic — Default base URL `https://api.anthropic.com/v1` - SDK: `@ai-sdk/anthropic ^3.0.76` via `createAnthropic(...).languageModel(model)` - Google Gemini — Default base URL `https://generativelanguage.googleapis.com/v1beta` - SDK: `@ai-sdk/google ^3.0.71` via `createGoogleGenerativeAI(...).languageModel(model)` - Vercel AI Gateway — Default base URL `https://ai-gateway.vercel.sh/v3/ai` - SDK: `ai ^6.0.177` via `createGateway(...).languageModel(model)` - OpenRouter — Default base URL `https://openrouter.ai/api/v1` - SDK: `@ai-sdk/openai-compatible ^2.0.47` via `createOpenAICompatible({ name: "openrouter", ... })` - Ollama — Default base URL `https://ollama.com/api` - SDK: `ollama-ai-provider-v2 ^3.5.0` via `createOllama(...)` - Security: `packages/api/src/services/ai.ts` `resolveBaseUrl` rejects non-HTTPS URLs, credentialed URLs, and private/loopback hosts (via `packages/utils/src/url-security.node.ts`). - File-input AI calls are capped at 10MB (`MAX_AI_FILE_BYTES`). **OAuth Identity Providers (server-side env-configured, optional):** - Google — `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` (config in `packages/auth/src/config.ts`). - GitHub — `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`. - LinkedIn — `LINKEDIN_CLIENT_ID` / `LINKEDIN_CLIENT_SECRET`. - Custom Generic OAuth — `OAUTH_PROVIDER_NAME`, `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, plus either `OAUTH_DISCOVERY_URL` or all three of `OAUTH_AUTHORIZATION_URL`/`OAUTH_TOKEN_URL`/`OAUTH_USER_INFO_URL`. Scopes from `OAUTH_SCOPES` (defaults `openid profile email`). Wired via Better Auth's `genericOAuth` plugin. - Trusted providers for account linking (`packages/auth/src/config.ts`): `google`, `github`, `linkedin`. **Translation / Localization:** - Crowdin — `CROWDIN_PROJECT_ID`, `CROWDIN_PERSONAL_TOKEN`. Config in `crowdin.yml`; pull request automation labelled `l10n`. Source catalog `apps/web/locales/en-US.po`. **Font Catalog Tooling:** - Google Fonts Developer API — `GOOGLE_CLOUD_API_KEY` consumed by `packages/scripts/fonts/generate.ts` hitting `https://www.googleapis.com/webfonts/v1/webfonts`. Output committed at `packages/fonts/src/webfontlist.json`. ## Data Storage **Databases:** - PostgreSQL — Required relational database. - Connection env: `DATABASE_URL` (validated as `postgres(ql)://...` in `packages/env/src/server.ts`) - Client: `drizzle-orm 1.0.0-beta.22` with `pg ^8.20.0` Pool, instantiated in `packages/db/src/client.ts` (singleton via `globalThis.__pool` / `globalThis.__drizzle`). - Schema location: `packages/db/src/schema/index.ts` (auth + resume tables) with `packages/db/src/relations.ts`. - Migrations: generated by `drizzle-kit` into repo-root `migrations/` (e.g. `20260507144406_fast_nova/`). Generator config at `packages/db/drizzle.config.ts`. - Auto-migration on app start: `apps/web/plugins/1.migrate.ts` runs `drizzle-orm/node-postgres/migrator` against the resolved migrations folder. - `drizzle-kit` does NOT auto-load `.env` — `DATABASE_URL` must be exported before running `pnpm db:generate` / `pnpm db:migrate` (see `AGENTS.md` "Important" callout). - Health probe: `packages/api/src/services/storage.ts` healthcheck + `apps/web/src/routes/api/health.ts` runs `SELECT 1` through Drizzle (1.5s timeout). **File Storage (selected at runtime in `packages/api/src/services/storage.ts`):** - S3-compatible object storage when all three of `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_BUCKET` are set. - Client: `@aws-sdk/client-s3 ^3.1045.0` (`S3Client` constructed in `S3StorageService`). - Options: `S3_REGION` (default `us-east-1`), `S3_ENDPOINT` (for non-AWS), `S3_FORCE_PATH_STYLE`. - Tested compatible backends: AWS S3 and SeaweedFS (see `compose.dev.yml` / `compose.yml`). - All writes use `ACL: "public-read"` (consumed publicly from `/uploads/$userId/$` route, with path validation). - Local filesystem fallback (`LocalStorageService`) used when any of the three S3 vars is missing. - Path: `LOCAL_STORAGE_PATH` if set (must be absolute); otherwise `/data` in dev or `/app/data` in the Docker image. - Validated at boot by `apps/web/plugins/2.storage.ts` (mkdir + access check). - Key layout (built in `packages/api/src/services/storage.ts`): - `uploads/{userId}/pictures/{timestamp}.jpeg` - `uploads/{userId}/screenshots/{resumeId}/{timestamp}.jpeg` - `uploads/{userId}/pdfs/{resumeId}/{timestamp}.pdf` - Public read route: `apps/web/src/routes/uploads/$userId.$.tsx` (ETag, content-type sniffing, path traversal protection). - Image preprocessing: `sharp ^0.34.5` resizes to 800x800 and re-encodes JPEG at quality 80, unless `FLAG_DISABLE_IMAGE_PROCESSING=true`. **Caching:** - No external cache (Redis/Memcached) configured. - In-process rate limiter: `@orpc/experimental-ratelimit` `MemoryRatelimiter` (`packages/api/src/middleware/rate-limit/index.ts`). - Workbox precache in the service worker (PWA) — `apps/web/vite.config.ts` `VitePWA` setup (skipWaiting, clientsClaim, cleanupOutdatedCaches). ## Authentication & Identity **Auth Provider:** Better Auth `1.6.10`, configured in `packages/auth/src/config.ts`. - Mounted as a TanStack Start server handler at `/api/auth/*` via `apps/web/src/routes/api/auth.$.ts`. - Drizzle adapter: `@better-auth/drizzle-adapter` bound to `db` + schema (provider `pg`). - Trusted origins: `http://localhost:3000`, `http://127.0.0.1:3000`, and the normalized origin of `APP_URL`. - Secure cookies enabled automatically when `APP_URL` is `https://`. - Trusted IP headers (used by both Better Auth `advanced.ipAddress.ipAddressHeaders` and the oRPC rate limiter): `CF-Connecting-IP`, `CF-Connecting-IPv6`, `True-Client-IP`, `X-Forwarded-For`, `X-Real-IP` (`packages/utils/src/rate-limit.ts`). - Telemetry: explicitly disabled. **Email/Password:** - Enabled unless `FLAG_DISABLE_EMAIL_AUTH=true`. - Password hash: `bcrypt` (cost 10) via `hash` / `compare` from `bcrypt ^6.0.0`. - Min 8, max 64 char passwords. - Email verification: sent on signup using `VerifyEmail` template from `@reactive-resume/email/templates/auth`. - Reset password: `sendResetPassword` -> `ResetPasswordEmail` template. - Email change: `sendChangeEmailConfirmation` -> `VerifyEmailChange` template. **Better Auth Plugins (in `packages/auth/src/config.ts`):** - `jwt()` — JWKS published at `/api/auth/jwks` (also referenced by `verifyOAuthToken`). - `admin()` — Admin operations. - `passkey()` (`@better-auth/passkey ^1.6.10`) — WebAuthn passkeys. - `genericOAuth({ config })` — Driven by `OAUTH_*` env vars when configured. - `twoFactor({ issuer: "Reactive Resume" })` — TOTP + backup codes; UI routes `/auth/verify-2fa` and `/auth/verify-2fa-backup`. - `apiKey()` (`@better-auth/api-key ^1.6.10`) — Header `x-api-key`; `enableSessionForAPIKeys: true`; per-key rate limit 1000 req/hour. - `oauthProvider()` (`@better-auth/oauth-provider ^1.6.10`) — Makes this app act as an OAuth 2.1 authorization server for MCP clients. Allows dynamic and unauthenticated client registration (RFC 7591) but redirect URIs are gated by an allowlist in the auth `hooks.before` middleware and `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`. Audiences whitelist: `${APP_URL}`, `${APP_URL}/`, `${APP_URL}/mcp`, `${APP_URL}/mcp/`. - `username()` — Normalized lowercase usernames (`^[a-z0-9._-]+$`, length 3–64). - `dash({ apiKey: BETTER_AUTH_API_KEY })` — Better Auth Dashboard (only when `BETTER_AUTH_API_KEY` is set). **Social Providers:** - Google, GitHub, LinkedIn — Each activates only when both `*_CLIENT_ID` and `*_CLIENT_SECRET` env vars are set (`packages/auth/src/config.ts`). `disableImplicitSignUp: true`; account linking enabled. **Session / Access in API context:** - `packages/api/src/context.ts` exposes `protectedProcedure` (referenced in `AGENTS.md`) for authenticated procedures. - oRPC middleware reads request headers via `RequestHeadersPlugin` (`apps/web/src/routes/api/rpc.$.ts`, `apps/web/src/routes/api/openapi.$.ts`). **API Key Auth for HTTP / MCP:** - `x-api-key` header recognised by Better Auth API Key plugin. - OpenAPI spec at `/api/openapi/spec.json` declares `apiKey` security scheme (`apps/web/src/routes/api/openapi.$.ts`). - MCP server first tries OAuth Bearer (`Authorization: Bearer ...`), then falls back to `x-api-key` (`apps/web/src/routes/mcp/index.ts`). ## Monitoring & Observability **Error Tracking:** - None — no Sentry/Datadog/Rollbar SDK present. - Errors are logged via `console.error` (oRPC interceptors `onError` in `apps/web/src/routes/api/rpc.$.ts` and `apps/web/src/routes/api/openapi.$.ts`). **Health Checks:** - `GET /api/health` — `apps/web/src/routes/api/health.ts` returns JSON with DB and storage status; 503 if unhealthy. Used by Docker `HEALTHCHECK`. **Logs:** - `console.info`/`console.warn`/`console.error` only. SMTP failures, missing config, sanitization diagnostics, and migration progress are all written to stdout/stderr. ## CI/CD & Deployment **Hosting / Distribution:** - Self-hosted Docker image — `Dockerfile` builds final image around `node:24-slim`, expose port 3000, default `LOCAL_STORAGE_PATH=/app/data`. Multi-stage uses `turbo prune` for both the web app and the `runtime-externals` package (which carries `bcrypt`, `sharp`, `@aws-sdk/client-s3` as native deps). - Container labels point to `https://rxresu.me` and `https://docs.rxresu.me`. **CI Pipeline:** - Not present in this worktree (no `.github/workflows/` enumerated here). Scripts produce CI-friendly Vitest reports (`reports/vitest-junit.xml`, `reports/vitest-results.json`) via the `test:ci` task. **Local Orchestration:** - `compose.dev.yml` — `postgres:latest`, `seaweedfs:latest`, `seaweedfs_create_bucket` (uses `quay.io/minio/mc:latest`). - `compose.yml` — Same services plus `reactive_resume` app container, networks `data_network` / `storage_network`. **Git Hooks:** - Lefthook (`lefthook.yml`) — `pre-commit` runs `biome check --write --unsafe` on staged JS/TS/JSON files; `commit-msg` runs `commitlint --edit`. ## Environment Configuration **Required env vars (validated by Zod in `packages/env/src/server.ts`):** - `APP_URL` — http(s) URL, used for auth base URL, OAuth audiences, OG metadata, and public upload URLs. - `DATABASE_URL` — `postgres(ql)://...`. - `AUTH_SECRET` — Better Auth signing secret (`openssl rand -hex 32`). **Optional auth env vars:** - `BETTER_AUTH_API_KEY` — Enables Better Auth Dashboard plugin. - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`. - `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`. - `OAUTH_PROVIDER_NAME`, `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_DISCOVERY_URL`, `OAUTH_AUTHORIZATION_URL`, `OAUTH_TOKEN_URL`, `OAUTH_USER_INFO_URL`, `OAUTH_SCOPES`, `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`. **Optional SMTP env vars (all required together to enable real sends):** - `SMTP_HOST`, `SMTP_PORT` (default 587), `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_SECURE` (default false). Fallback in `packages/email/src/transport.ts`: log to console with subject/body when SMTP is not fully configured. **Optional storage env vars:** - `LOCAL_STORAGE_PATH` — Must be absolute when set. - `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION` (default `us-east-1`), `S3_ENDPOINT`, `S3_BUCKET`, `S3_FORCE_PATH_STYLE` (default false). **Optional feature flags:** - `FLAG_DISABLE_SIGNUPS` — Disables signups across all providers. - `FLAG_DISABLE_EMAIL_AUTH` — Disables email/password auth and verification flows. - `FLAG_DISABLE_IMAGE_PROCESSING` — Skips Sharp resize/encode (useful on resource-constrained hardware). **Optional tooling env vars:** - `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN` for Crowdin translation sync. - `GOOGLE_CLOUD_API_KEY` — For `packages/scripts/fonts/generate.ts`. **Secrets location:** - `.env` at the repo root (gitignored). `.env.example` provides documented defaults. Existence noted: `.env.local`, `.env.production` files present locally (contents not read; may contain secrets). - Turbo cache invalidation tied to env vars via `turbo.json` `globalEnv` whitelist. ## Webhooks & Callbacks **Incoming:** - OAuth provider callbacks served by Better Auth: `/api/auth/oauth2/callback/{providerId}` (e.g. `/api/auth/oauth2/callback/custom` configured in `packages/auth/src/config.ts`). - OAuth Authorization Server endpoints (when this app acts as an OAuth provider for MCP clients): handled by Better Auth `oauthProvider` plugin under `/api/auth/oauth2/*`. Login/consent pages at `/auth/oauth` (`apps/web/src/routes/auth/oauth.ts`). - `.well-known` discovery routes: - `apps/web/src/routes/[.]well-known/oauth-authorization-server.ts` and `.../oauth-authorization-server.$.ts` - `apps/web/src/routes/[.]well-known/oauth-protected-resource.ts` and `.../oauth-protected-resource.$.ts` - `apps/web/src/routes/[.]well-known/openid-configuration.ts` - `apps/web/src/routes/[.]well-known/mcp/server-card[.]json.ts` - Generic catch-all: `apps/web/src/routes/[.]well-known/$.ts` - MCP Streamable HTTP transport: `apps/web/src/routes/mcp/index.ts` (uses `WebStandardStreamableHTTPServerTransport`). Authenticates via OAuth Bearer or `x-api-key`. **Outgoing:** - AI provider HTTPS requests (see "AI Providers" above) — initiated per user request from `packages/api/src/services/ai.ts`. - SMTP outbound (`packages/email/src/transport.ts`) for verification, password reset, and email-change confirmations. - S3 PUT/GET/DELETE/LIST through `@aws-sdk/client-s3` when S3 storage is configured. - Crowdin and Google Fonts requests only from CI / scripts, not from the runtime web app. ## API Endpoints Exposed by the App | Endpoint | Handler | Purpose | |----------|---------|---------| | `/api/health` | `apps/web/src/routes/api/health.ts` | Liveness/readiness JSON (DB + storage). | | `/api/auth/*` | `apps/web/src/routes/api/auth.$.ts` | Better Auth handler (sessions, OAuth, passkey, JWKS, etc.). | | `/api/rpc/*` | `apps/web/src/routes/api/rpc.$.ts` | oRPC server (`packages/api/src/routers/index.ts`: `ai`, `auth`, `flags`, `resume`, `statistics`, `storage`). | | `/api/openapi/*` | `apps/web/src/routes/api/openapi.$.ts` | OpenAPI-style REST wrapper of the oRPC router; spec at `/api/openapi/spec.json`. Auth via `x-api-key`. | | `/mcp` | `apps/web/src/routes/mcp/index.ts` | MCP Streamable HTTP server (`@modelcontextprotocol/sdk`). | | `/uploads/{userId}/...` | `apps/web/src/routes/uploads/$userId.$.tsx` | Public read of stored images/PDFs with ETag + path validation. | | `/schema.json` | `apps/web/src/routes/schema[.]json.ts` | Resume JSON schema. | | `/.well-known/...` | `apps/web/src/routes/[.]well-known/*` | OAuth/OIDC/MCP discovery documents. | ## Rate Limiting **Better Auth (production only, see `packages/auth/src/config.ts` `isRateLimitEnabled`):** - Global default: 60 req / 60s. - `/sign-in/email`: 5/60s; `/sign-up/email`: 3/60s. - `/request-password-reset`, `/send-verification-email`: 3/600s. - `/two-factor/verify-otp`, `/verify-totp`, `/verify-backup-code`: 5/600s. - `/is-username-available`: 20/60s. - OAuth provider endpoints: `register` 5/60s, `authorize` 30/60s, `token` 20/60s, `introspect` 60/60s, `revoke` 30/60s, `userinfo` 60/60s. - API keys: 1000 req / hour per key. - Source: `packages/utils/src/rate-limit.ts`. **oRPC procedure-level (in-memory, production only, see `packages/api/src/middleware/rate-limit/index.ts`):** - `resumePassword`: 5 / 10min. - `pdfExport`: 5 / 60s. - `aiRequest`: 20 / 60s. - `jobsSearch`: 30 / 60s. - `jobsTestConnection`: 10 / 60s. - `storageUpload`: 20 / 60s. - `storageDelete`: 30 / 60s. - `resumeMutations`: 60 / 60s. - Keys are derived from authenticated user id when present, otherwise from the first trusted-IP header or a UA+language fingerprint. ## Optional Services Declared in Compose Files | Service | Image | Purpose | File | |---------|-------|---------|------| | `postgres` | `postgres:latest` | Required PostgreSQL DB | `compose.dev.yml`, `compose.yml` | | `seaweedfs` | `chrislusf/seaweedfs:latest` | Optional S3-compatible storage for dev/prod | `compose.dev.yml`, `compose.yml` | | `seaweedfs_create_bucket` | `quay.io/minio/mc:latest` | One-shot init that creates `reactive-resume` bucket | `compose.dev.yml`, `compose.yml` | | `reactive_resume` | Built from `Dockerfile` | The app itself in prod compose | `compose.yml` | --- *Integration audit: 2026-05-11*