|
| 1 | +# sfetch — Agent Rules & Project Conventions |
| 2 | + |
| 3 | +## What this project is |
| 4 | + |
| 5 | +A locally-run Docker-based Salesforce-to-PostgreSQL data pipeline. The database is the product. External apps connect directly to Postgres. A React web UI handles configuration. Everything runs via `docker compose up`. |
| 6 | + |
| 7 | +Full scope: `docs/scope.md` |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Commit Rules for Agents |
| 12 | + |
| 13 | +**Agents must commit their work after completing each task.** Do not batch multiple tasks into one commit. One task = one commit (minimum). |
| 14 | + |
| 15 | +### Commit format |
| 16 | + |
| 17 | +``` |
| 18 | +<type>(<scope>): <short description> |
| 19 | +
|
| 20 | +<optional body — what changed and why> |
| 21 | +
|
| 22 | +Co-Authored-By: Claude <noreply@anthropic.com> |
| 23 | +``` |
| 24 | + |
| 25 | +**Types:** |
| 26 | +- `feat` — new functionality |
| 27 | +- `fix` — bug fix |
| 28 | +- `chore` — config, tooling, setup |
| 29 | +- `refactor` — restructure without behavior change |
| 30 | +- `docs` — documentation only |
| 31 | + |
| 32 | +**Scopes:** `scaffold`, `docker`, `db`, `auth`, `bulk-api`, `ddl`, `delta-sync`, `reconciliation`, `scheduler`, `api`, `ui`, `build` |
| 33 | + |
| 34 | +**Examples:** |
| 35 | +``` |
| 36 | +feat(auth): add SF org token reader from ~/.sf JSON files |
| 37 | +feat(delta-sync): implement initial full load when last_delta_sync is NULL |
| 38 | +chore(scaffold): initialize project structure and package.json files |
| 39 | +feat(ui): add objects page with sync toggle and row count display |
| 40 | +``` |
| 41 | + |
| 42 | +### When to commit |
| 43 | + |
| 44 | +- After completing a full task from the task list |
| 45 | +- After a meaningful sub-step within a large task (e.g. after each route module, after each UI page) |
| 46 | +- Always before starting a new task |
| 47 | +- Never commit broken or partially-wired code — a commit should represent a working unit |
| 48 | + |
| 49 | +### What to stage |
| 50 | + |
| 51 | +Stage specific files by name — never `git add .` or `git add -A` blindly. Check `git status` first. Never stage: |
| 52 | +- `data/` (git-ignored, but double-check) |
| 53 | +- `.env` (git-ignored, but double-check) |
| 54 | +- Unrelated files touched incidentally |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## Branch Strategy |
| 59 | + |
| 60 | +All work goes to `main` for this project. It is a local-only tool with a single developer. No feature branches required unless explicitly requested. |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## Tech Stack (quick reference) |
| 65 | + |
| 66 | +| Layer | Technology | |
| 67 | +|---|---| |
| 68 | +| Database | PostgreSQL (Docker) | |
| 69 | +| Backend | Node.js + TypeScript + Express | |
| 70 | +| Frontend | React + TypeScript + shadcn/ui + Tailwind | |
| 71 | +| SF Auth | Read `~/.sf/` JSON directly (no sf binary in container) | |
| 72 | +| SF Data | jsforce + Bulk API 2.0 | |
| 73 | +| Scheduler | node-cron | |
| 74 | +| Containers | Docker + Docker Compose | |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## Directory Structure |
| 79 | + |
| 80 | +``` |
| 81 | +sf-db/ |
| 82 | +├── src/ |
| 83 | +│ ├── api/ # Express backend + sync engine |
| 84 | +│ └── ui/ # React frontend |
| 85 | +├── docker/ # Dockerfiles only |
| 86 | +├── data/ # LOCAL ONLY — git-ignored |
| 87 | +│ ├── docker/ # Postgres volume mount |
| 88 | +│ └── downloads/ # Future file exports |
| 89 | +├── docs/ # Project documentation |
| 90 | +├── docker-compose.yml |
| 91 | +├── .env # Git-ignored — ports, DB creds, LOG_RETENTION_DAYS |
| 92 | +├── .env.example # Committed — template with empty values |
| 93 | +└── CLAUDE.md |
| 94 | +``` |
| 95 | + |
| 96 | +--- |
| 97 | + |
| 98 | +## Code Conventions |
| 99 | + |
| 100 | +### TypeScript |
| 101 | +- Strict mode on (`"strict": true` in tsconfig) |
| 102 | +- No `any` — use `unknown` and narrow properly |
| 103 | +- Prefer `interface` over `type` for object shapes |
| 104 | +- Async/await over raw promises |
| 105 | +- All database queries go through the pg pool — never create ad-hoc connections |
| 106 | + |
| 107 | +### Postgres |
| 108 | +- Synced Salesforce data → `salesforce` schema |
| 109 | +- Internal app tables → `sfdb` schema |
| 110 | +- Every synced table must have: `id`, `sf_created_at`, `sf_updated_at`, `sf_deleted_at`, `synced_at` |
| 111 | +- Field names are lowercase snake_case versions of SF API names |
| 112 | +- DDL is always idempotent (`IF NOT EXISTS` / `IF EXISTS`) |
| 113 | + |
| 114 | +### Sync engine |
| 115 | +- Always acquire `sfdb.sync_lock` before running any sync |
| 116 | +- Always release the lock in a `finally` block — never leave it held on error |
| 117 | +- If `last_delta_sync` is NULL → initial full load (no SystemModstamp WHERE clause) |
| 118 | +- Stale lock threshold: 30 minutes |
| 119 | +- Log purge runs at the start of every sync (delete rows older than `LOG_RETENTION_DAYS`) |
| 120 | + |
| 121 | +### API |
| 122 | +- All routes under `/api/` prefix |
| 123 | +- Non-API routes serve the React SPA (`dist/index.html`) |
| 124 | +- Return consistent error shape: `{ error: string, details?: unknown }` |
| 125 | +- No authentication on API — local-only tool, localhost only |
| 126 | + |
| 127 | +### React / UI |
| 128 | +- Components in `src/ui/src/components/` |
| 129 | +- Pages in `src/ui/src/pages/` |
| 130 | +- API calls through a single typed client (`src/ui/src/lib/api.ts`) |
| 131 | +- Use shadcn/ui components — do not build primitives from scratch |
| 132 | +- Confirmation modals required before any destructive action (drop column, drop table) |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## Environment Variables |
| 137 | + |
| 138 | +Only bootstrap values live in `.env` — values needed before the DB exists. |
| 139 | + |
| 140 | +```env |
| 141 | +POSTGRES_USER=sfdb |
| 142 | +POSTGRES_PASSWORD=changeme |
| 143 | +POSTGRES_PORT=7745 |
| 144 | +APP_PORT=7743 |
| 145 | +LOG_RETENTION_DAYS=14 |
| 146 | +``` |
| 147 | + |
| 148 | +All runtime config (active org alias, sync intervals, enabled objects/fields) lives in the `sfdb` schema in the database. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Key Design Decisions (do not revisit without good reason) |
| 153 | + |
| 154 | +- **sf CLI binary is NOT in the Docker image.** Auth tokens are read directly from the `~/.sf/` JSON files mounted into the container. No `sf org display` command. |
| 155 | +- **The API is not a data API.** It serves the UI and orchestrates syncs only. External tools connect directly to Postgres. |
| 156 | +- **Deletions are soft.** `sf_deleted_at` is set — records are never hard-deleted from the local DB. |
| 157 | +- **Bulk API 2.0 by default.** REST query fallback only for objects under 2,000 records. |
| 158 | +- **Config in DB, not `.env`.** `.env` is infrastructure only. Org alias, object selection, field selection, and schedule config all live in `sfdb.app_config` / `sfdb.sync_config` / `sfdb.field_config`. |
| 159 | +- **One active org at a time.** Multi-org simultaneous sync is out of scope for v1. |
0 commit comments