@pi-stef/finance-api 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # @pi-stef/finance-api
2
2
 
3
- Always-on local service for financial data ingestion, storage, and deterministic quant analysis.
3
+ Always-on local service for financial data ingestion, storage, and deterministic quant analysis. Backed by SQLite; serves a bearer-token-authenticated HTTP API to the `@pi-stef/finance` extension and any other client.
4
4
 
5
- ## Install
5
+ ---
6
6
 
7
- ### Docker (Recommended)
7
+ ## Quick start
8
+
9
+ ### Docker (recommended)
8
10
 
9
11
  ```bash
10
12
  cd packages/finance-api/docker
11
- docker compose up --build
13
+ docker compose up -d
12
14
  ```
13
15
 
14
- The service will be available at `http://127.0.0.1:7780`.
16
+ Pulls `ghcr.io/sfiorini/pi-stef/finance-api:latest` and starts the service at `http://127.0.0.1:7780`. See the [Docker guide](docker/README.md) for image tags, volumes, and retrieving the token.
15
17
 
16
18
  ### Native
17
19
 
@@ -22,20 +24,53 @@ pnpm serve
22
24
 
23
25
  See [docs/native-run.md](docs/native-run.md) for launchd/systemd setup.
24
26
 
27
+ ### Verify
28
+
29
+ ```bash
30
+ curl http://127.0.0.1:7780/v1/health
31
+ # {"ok":true,"data":{"status":"ok","uptimeS":0}}
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Authentication
37
+
38
+ All endpoints except `/v1/health` require a bearer token via the `Authorization` header:
39
+
40
+ ```
41
+ Authorization: Bearer <token>
42
+ ```
43
+
44
+ **Token lifecycle:**
45
+
46
+ - On first start, the service generates a random UUID token and writes it to `~/.pi/sf/finance/token` (`chmod 600`), created atomically and race-safe via `O_EXCL`.
47
+ - The token is stable across restarts as long as the token file persists.
48
+ - In Docker, the token is stored inside the container at `/root/.pi/sf/finance/token` and persists via the `finance-config` volume. Retrieve it with:
49
+ ```bash
50
+ docker compose exec finance-api cat /root/.pi/sf/finance/token
51
+ ```
52
+
53
+ **Override:** Set `SF_FINANCE_TOKEN` to pin a specific token (useful for CI or sharing across hosts).
54
+
55
+ The `@pi-stef/finance` extension reads this token automatically when co-located on the same host.
56
+
57
+ ---
58
+
25
59
  ## Configuration
26
60
 
27
- Environment variables (prefix `SF_FINANCE_`):
61
+ All configuration is via environment variables (prefix `SF_FINANCE_`):
28
62
 
29
63
  | Variable | Default | Description |
30
64
  |----------|---------|-------------|
31
- | `SF_FINANCE_HOST` | `127.0.0.1` | Server host (use `0.0.0.0` for Docker) |
65
+ | `SF_FINANCE_HOST` | `127.0.0.1` (`0.0.0.0` in Docker) | Server bind host |
32
66
  | `SF_FINANCE_PORT` | `7780` | Server port |
33
- | `SF_FINANCE_DB` | `~/.pi/sf/finance/finance.db` | SQLite database path |
34
- | `SF_FINANCE_DATA_FEED` | `stooq` | Price data feed (`stooq` or `yfinance`) |
67
+ | `SF_FINANCE_DB` | `~/.pi/sf/finance/finance.db` (`/data/finance.db` in Docker) | SQLite database path |
68
+ | `SF_FINANCE_TOKEN` | (auto-generated) | Bearer token (overrides the token file) |
69
+ | `SF_FINANCE_DATA_FEED` | `stooq` | Price data feed (`stooq`) |
35
70
 
36
- ## Secrets
71
+ ### Secrets (`secrets.json`)
37
72
 
38
- Create `~/.pi/sf/finance/secrets.json` with provider credentials:
73
+ Create `~/.pi/sf/finance/secrets.json` with provider credentials. The file is `chmod 600` on creation.
39
74
 
40
75
  ```json
41
76
  {
@@ -52,55 +87,280 @@ Create `~/.pi/sf/finance/secrets.json` with provider credentials:
52
87
  }
53
88
  ```
54
89
 
55
- The file is automatically `chmod 600` on creation.
90
+ Each provider's required credentials are documented under [Providers](#providers).
91
+
92
+ ---
56
93
 
57
94
  ## Providers
58
95
 
59
- | Provider | Kind | Auth | Status |
60
- |----------|------|------|--------|
96
+ | Provider | Kind | Auth (in `secrets.json`) | Status |
97
+ |----------|------|--------------------------|--------|
61
98
  | File Import (CSV/OFX) | brokerage/banking | `filePath` | ✅ Working |
62
99
  | Coinbase | crypto | `keyName` + `privateKey` | ⚠️ Stub (HMAC not implemented) |
63
100
  | SnapTrade | brokerage | `clientId` + `consumerKey` | ⚠️ Stub |
64
101
  | SimpleFIN | banking | `accessKey` | ⚠️ Stub |
65
102
  | Teller | banking | `token` | ⚠️ Stub |
66
103
 
67
- ## First Run
68
-
69
- 1. Start the service
70
- 2. Import holdings: `POST /v1/import {"filePath": "positions.csv"}`
71
- 3. Set investment goal: `POST /v1/goals {"id": "g1", "name": "Growth", "targetAllocation": {"equity": 0.8, "bonds": 0.2}}`
72
- 4. Check drift: `GET /v1/drift`
73
-
74
- ## API
75
-
76
- All endpoints (except `/v1/health`) require `Authorization: Bearer <token>` header.
77
-
78
- | Method | Path | Description |
79
- |--------|------|-------------|
80
- | GET | `/v1/health` | Health check (public) |
81
- | GET | `/v1/market-status` | Current market session |
82
- | GET | `/v1/holdings` | All holdings |
83
- | GET | `/v1/net-worth` | Total portfolio value |
84
- | GET | `/v1/drift` | Allocation drift |
85
- | GET | `/v1/allocation` | Current allocation |
86
- | GET | `/v1/goals` | Investment goals |
87
- | POST | `/v1/goals` | Create/update goal |
88
- | GET | `/v1/suggestions` | Pending suggestions |
89
- | POST | `/v1/suggestions/dismiss` | Dismiss suggestion |
90
- | POST | `/v1/sync` | Trigger sync |
91
- | POST | `/v1/import` | Import from file |
92
- | GET | `/v1/history` | Price history |
93
- | POST | `/v1/export` | Export data |
104
+ **Provider setup:**
105
+
106
+ - **File Import (CSV)** — Export positions from your brokerage (e.g. Fidelity's Positions download) and point `filePath` at the CSV. Supported columns: symbol, quantity, last price. Call `POST /v1/import` with the path to ingest.
107
+ - **File Import (OFX)** Export transactions from your bank in OFX format and point `filePath` at it.
108
+ - **Coinbase / SnapTrade / SimpleFIN / Teller** Stubs in the current release. Credentials are accepted and validated against the contract, but live API calls are not yet implemented. Tracked for a future release.
109
+
110
+ ---
111
+
112
+ ## HTTP API reference
113
+
114
+ Base URL: `http://127.0.0.1:7780`. All endpoints return `{ "ok": true, "data": {...} }` on success or `{ "ok": false, "error": { "code": "...", "message": "..." } }` on failure.
115
+
116
+ ### `GET /v1/health` *(public)*
117
+
118
+ Health check; no auth required.
119
+
120
+ ```json
121
+ { "ok": true, "data": { "status": "ok", "uptimeS": 123 } }
122
+ ```
123
+
124
+ ### `GET /v1/market-status`
125
+
126
+ Returns the current US market session classification.
127
+
128
+ ```json
129
+ { "ok": true, "data": { "session": "regular", "timestamp": 1782000000000 } }
130
+ ```
131
+
132
+ `session` is one of `pre`, `regular`, `post`, `closed`. Holiday list currently covers 2026.
133
+
134
+ ### `GET /v1/holdings`
135
+
136
+ Accounts and their holdings.
137
+
138
+ ```json
139
+ { "ok": true, "data": { "accounts": [
140
+ { "id": "fidelity", "provider_id": "import", "kind": "brokerage", "name": "Fidelity",
141
+ "holdings": [ { "account_id": "fidelity", "symbol": "AAPL", "quantity": 10, "asset_class": "equity", "as_of": 1782000000000 } ] }
142
+ ] } }
143
+ ```
144
+
145
+ ### `GET /v1/net-worth`
146
+
147
+ Total portfolio value using latest prices (falls back to average cost).
148
+
149
+ ```json
150
+ { "ok": true, "data": { "netWorth": 123456.78, "accountCount": 3 } }
151
+ ```
152
+
153
+ ### `GET /v1/allocation`
154
+
155
+ Current asset allocation as flat weights by asset class.
156
+
157
+ ```json
158
+ { "ok": true, "data": { "allocation": { "equity": 0.72, "bonds": 0.18, "cash": 0.10 }, "totalValue": 123456.78 } }
159
+ ```
160
+
161
+ ### `GET /v1/drift`
162
+
163
+ Allocation drift vs the configured goal's target allocation.
164
+
165
+ ```json
166
+ { "ok": true, "data": { "drift": [
167
+ { "class": "equity", "currentPct": 0.72, "targetPct": 0.80, "deltaPct": -0.08, "value": 88888.0 }
168
+ ] } }
169
+ ```
170
+
171
+ ### `GET /v1/goals`
172
+
173
+ List investment goals (target allocation is parsed from stored JSON).
174
+
175
+ ```json
176
+ { "ok": true, "data": { "goals": [
177
+ { "id": "g1", "name": "Growth", "targetAllocation": { "equity": 0.8, "bonds": 0.2 }, "riskLimits": {}, "horizon_years": 10 }
178
+ ] } }
179
+ ```
180
+
181
+ > Note: `target_allocation` and `risk_limits` are camelCased in the response (`targetAllocation`/`riskLimits`, parsed from JSON); `horizon_years` keeps its snake_case DB form.
182
+
183
+ ### `POST /v1/goals`
184
+
185
+ Create or update (UPSERT) an investment goal. Validates that the target allocation sums to ~1.0.
186
+
187
+ **Request body:**
188
+
189
+ ```json
190
+ {
191
+ "id": "g1",
192
+ "name": "Growth",
193
+ "targetAllocation": { "equity": 0.8, "bonds": 0.2 },
194
+ "riskLimits": { "maxConcentration": 0.25 },
195
+ "horizonYears": 10
196
+ }
197
+ ```
198
+
199
+ | Field | Type | Required | Description |
200
+ |-------|------|----------|-------------|
201
+ | `id` | string | yes | Goal identifier |
202
+ | `name` | string | yes | Display name |
203
+ | `targetAllocation` | object | yes | Asset-class weights (must sum to ~1.0) |
204
+ | `riskLimits` | object | no | Risk limits (e.g. `maxConcentration`) |
205
+ | `horizonYears` | number | no | Investment horizon |
206
+
207
+ ```json
208
+ { "ok": true, "data": { "id": "g1" } }
209
+ ```
210
+
211
+ ### `GET /v1/suggestions`
212
+
213
+ Pending rebalance/risk/drift suggestions computed by the quant engine. Each suggestion's `payload` is parsed from stored JSON.
214
+
215
+ ```json
216
+ { "ok": true, "data": { "suggestions": [
217
+ { "id": "s-...-0", "kind": "rebalance", "status": "pending", "payload": { "symbol": "AAPL", "action": "buy", "amount": 500 } }
218
+ ] } }
219
+ ```
220
+
221
+ ### `POST /v1/suggestions/dismiss`
222
+
223
+ Dismiss a suggestion by id.
224
+
225
+ **Request body:** `{ "id": "s-...-0" }`
226
+
227
+ ```json
228
+ { "ok": true, "data": { "dismissed": "s-...-0" } }
229
+ ```
230
+
231
+ ### `POST /v1/sync`
232
+
233
+ Trigger a full scheduler tick: ingest from all configured providers, refresh prices, recompute suggestions.
234
+
235
+ ```json
236
+ { "ok": true, "data": {
237
+ "message": "Sync complete",
238
+ "session": "regular",
239
+ "accountsIngested": 3,
240
+ "holdingsIngested": 12,
241
+ "pricesUpdated": 12,
242
+ "suggestionsCreated": 2,
243
+ "errors": []
244
+ } }
245
+ ```
246
+
247
+ ### `POST /v1/import`
248
+
249
+ Import holdings from a local file (CSV or OFX). Absolute paths are allowed (single-user local service); relative paths containing `..` are rejected.
250
+
251
+ **Request body:** `{ "filePath": "/Users/me/Downloads/fidelity-positions.csv" }`
252
+
253
+ ```json
254
+ { "ok": true, "data": { "message": "Import complete", "filePath": "...", "accounts": 1 } }
255
+ ```
256
+
257
+ ### `GET /v1/history?symbol=AAPL[&accountId=...]`
258
+
259
+ Price history for a symbol, newest first. `accountId` optionally filters to prices relevant to a holding in that account.
260
+
261
+ ```json
262
+ { "ok": true, "data": { "history": [
263
+ { "symbol": "AAPL", "date": 1782000000000, "close": 210.5, "source": "stooq" }
264
+ ] } }
265
+ ```
266
+
267
+ ### `POST /v1/export`
268
+
269
+ Export data. `format: json` returns all tables inline; `format: sqlite` writes a backup copy of the database (restricted to the finance backup directory).
270
+
271
+ **Request body:** `{ "format": "json" }` or `{ "format": "sqlite", "path": "backup.db" }`
272
+
273
+ ```json
274
+ // json
275
+ { "ok": true, "data": { "holdings": [...], "prices": [...], ... } }
276
+ // sqlite
277
+ { "ok": true, "data": { "backupPath": "/home/.pi/sf/finance/backups/backup.db" } }
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Data model
283
+
284
+ SQLite, stored at `SF_FINANCE_DB`. Versioned migrations (see `src/store/schema.ts`); future changes add migration entries rather than mutating existing tables.
285
+
286
+ | Table | Purpose |
287
+ |-------|---------|
288
+ | `accounts` | Linked accounts (provider, kind, name, mask, currency, staleness) |
289
+ | `holdings` | Current holdings per account/symbol (quantity, avg cost, asset class, as-of) |
290
+ | `transactions` | Transactions (date, symbol, qty, price, type, fees) |
291
+ | `prices` | Price history per symbol/date (close, source) |
292
+ | `lots` | Tax lots per holding (open date, qty, cost basis) |
293
+ | `goals` | Investment goals (target allocation, risk limits, horizon) |
294
+ | `suggestion_records` | Persisted suggestions (kind, payload, status) |
295
+ | `market_sessions` | Cached market-session snapshots per date |
296
+
297
+ ---
298
+
299
+ ## Scheduler & quant engine
300
+
301
+ The built-in scheduler (`src/scheduler/`) runs a periodic tick whose cadence depends on the market session: more frequent intraday, hourly after hours, and every few hours when closed. Each tick:
302
+
303
+ 1. **Ingests** fresh data from configured providers via the provider registry.
304
+ 2. **Refreshes prices** from the configured data feed (default `stooq`).
305
+ 3. **Recomputes suggestions** deterministically through the quant engine:
306
+ - **Drift** — current vs target allocation deltas
307
+ - **Rebalance** — buy/sell amounts to return to target
308
+ - **Risk** — concentration and cash-drag checks against `riskLimits`
309
+ - **DCA** — dollar-cost-averaging recommendations (where configured)
310
+
311
+ **Determinism:** all numbers are computed by pure functions in `src/quant/`. The LLM client applies judgment but never recomputes the figures. This keeps suggestions reproducible and auditable.
312
+
313
+ `POST /v1/sync` triggers a tick on demand.
314
+
315
+ ---
316
+
317
+ ## Backup & restore
318
+
319
+ - **JSON export:** `POST /v1/export {"format":"json"}` returns all data inline.
320
+ - **SQLite backup:** `POST /v1/export {"format":"sqlite"}` writes a timestamped `.db` copy to `~/.pi/sf/finance/backups/` (path is sandboxed to that directory).
321
+ - **Restore:** stop the service, replace `SF_FINANCE_DB` with the backup file, restart.
322
+
323
+ ---
324
+
325
+ ## Observability
326
+
327
+ Structured logs are emitted to stdout (JSON) with `level`, `msg`, and contextual fields. Key events: server start, ingest results, staleness warnings, tick summaries. Increase verbosity via your process supervisor's log level (the service logs at `info` by default).
328
+
329
+ ---
330
+
331
+ ## Security model
332
+
333
+ - **Local-first:** bind to `127.0.0.1` by default. Docker maps `127.0.0.1:7780:7780` (localhost only) so the service is not exposed to the LAN.
334
+ - **Bearer auth:** every non-health endpoint requires a token; compared with `timingSafeEqual`.
335
+ - **Secrets:** `secrets.json` is `chmod 600`; provider credentials never leave the host.
336
+ - **File imports:** absolute paths are allowed (local file access by design); relative `..` traversal is rejected.
337
+ - **Backups:** the export route sandboxes SQLite backups to the finance backup directory.
338
+
339
+ ---
340
+
341
+ ## Troubleshooting
342
+
343
+ | Symptom | Fix |
344
+ |---------|-----|
345
+ | `401 Unauthorized` | Retrieve/regenerate the token (see [Authentication](#authentication)); check `SF_FINANCE_TOKEN` |
346
+ | Port already in use | Change `SF_FINANCE_PORT` and the compose port mapping |
347
+ | Stale holdings | Run `POST /v1/sync`; check provider credentials in `secrets.json` |
348
+ | `better-sqlite3` build fails (native) | Use the Docker image, or ensure `python3 make g++` are installed |
349
+ | No suggestions after sync | Set a goal via `POST /v1/goals` — drift/rebalance need a target |
350
+
351
+ ---
94
352
 
95
353
  ## Cost
96
354
 
97
- - **Free tier**: File imports (CSV/OFX) — no API costs
98
- - **Optional**: Coinbase API (free, view-only scope)
99
- - **Optional**: SnapTrade/SimpleFIN/Teller aggregators (may have fees)
355
+ - **Free tier:** File imports (CSV/OFX) and `stooq` prices — no API costs.
356
+ - **Optional:** Coinbase API (free, view-only scope) — currently a stub.
357
+ - **Optional:** SnapTrade / SimpleFIN / Teller aggregators (may have fees) — currently stubs.
358
+
359
+ ---
100
360
 
101
361
  ## Disclaimer
102
362
 
103
- **This is not financial advice.** The service provides deterministic calculations based on your data and configured goals. Suggestions are informational only — no trades are executed automatically.
363
+ **This is not financial advice.** The service provides deterministic calculations based on your data and configured goals. Suggestions are informational only — no trades are executed automatically. Always consult a qualified financial advisor before making investment decisions.
104
364
 
105
365
  ## License
106
366
 
package/docker/Dockerfile CHANGED
@@ -23,8 +23,15 @@ COPY packages/paths/src ./packages/paths/src
23
23
 
24
24
  # ---- runtime stage: slim, no build tooling ----
25
25
  FROM node:20-slim AS runtime
26
+ ARG OCI_VERSION=dev
26
27
  WORKDIR /app
27
28
 
29
+ # OCI labels for traceability (populated by buildx from metadata-action in CI)
30
+ LABEL org.opencontainers.image.title="@pi-stef/finance-api" \
31
+ org.opencontainers.image.source="https://github.com/sfiorini/pi-stef" \
32
+ org.opencontainers.image.licenses="MIT" \
33
+ org.opencontainers.image.version="${OCI_VERSION}"
34
+
28
35
  # Install curl for healthcheck
29
36
  RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
30
37
 
package/docker/README.md CHANGED
@@ -1,3 +1,123 @@
1
- # Docker packaging for finance-api
1
+ # finance-api Docker
2
2
 
3
- Dockerfile and docker-compose.yml will be added in M10 (Docker packaging).
3
+ The `@pi-stef/finance-api` service is published as a multi-arch Docker image to the GitHub Container Registry.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ cd packages/finance-api/docker
9
+ docker compose up -d
10
+ ```
11
+
12
+ This pulls `ghcr.io/sfiorini/pi-stef/finance-api:latest` and starts the service at `http://127.0.0.1:7780`.
13
+
14
+ Check it's running:
15
+
16
+ ```bash
17
+ curl http://127.0.0.1:7780/v1/health
18
+ # {"ok":true,"data":{"status":"ok","uptimeS":0}}
19
+ ```
20
+
21
+ ## Image
22
+
23
+ | Registry | Image |
24
+ |----------|-------|
25
+ | GHCR | `ghcr.io/sfiorini/pi-stef/finance-api` |
26
+
27
+ **Tags:**
28
+
29
+ - `latest` — most recent release
30
+ - `X.Y.Z` — pinned release (e.g. `0.1.0`)
31
+
32
+ **Platforms:** `linux/amd64`, `linux/arm64` (Intel Macs / Linux servers + Apple Silicon).
33
+
34
+ ```bash
35
+ # Pull a specific version
36
+ docker pull ghcr.io/sfiorini/pi-stef/finance-api:0.1.0
37
+ ```
38
+
39
+ The image is built from the repo source on every `@pi-stef/finance-api@X.Y.Z` tag push (see `.github/workflows/docker.yml`), so it always matches the released npm package.
40
+
41
+ ## Build from source (local dev)
42
+
43
+ To build the image locally instead of pulling from the registry:
44
+
45
+ ```bash
46
+ cd packages/finance-api/docker
47
+ # Uncomment the `build:` block in docker-compose.yml, then:
48
+ docker compose up --build
49
+ ```
50
+
51
+ Or build directly with `docker build`:
52
+
53
+ ```bash
54
+ docker build -f packages/finance-api/docker/Dockerfile -t finance-api:dev .
55
+ ```
56
+
57
+ The Dockerfile is a multi-stage source build. The build stage installs `python3`/`make`/`g++` to compile `better-sqlite3` native bindings; the runtime stage is slim and ships only the compiled app plus `curl` for healthchecks.
58
+
59
+ ## Configuration
60
+
61
+ All configuration is via environment variables (prefix `SF_FINANCE_`), set automatically by `docker-compose.yml`:
62
+
63
+ | Variable | Default | Description |
64
+ |----------|---------|-------------|
65
+ | `SF_FINANCE_HOST` | `0.0.0.0` (container) | Server bind host |
66
+ | `SF_FINANCE_PORT` | `7780` | Server port |
67
+ | `SF_FINANCE_DB` | `/data/finance.db` | SQLite database path |
68
+ | `SF_FINANCE_DATA_FEED` | `stooq` | Price data feed |
69
+
70
+ See the [service README](../README.md) for the full configuration and secrets reference.
71
+
72
+ ## Volumes
73
+
74
+ Two named volumes persist data across container restarts:
75
+
76
+ | Volume | Mount | Contents |
77
+ |--------|-------|----------|
78
+ | `finance-data` | `/data` | SQLite database (`finance.db`) |
79
+ | `finance-config` | `/root/.pi/sf/finance` | Auto-generated bearer token + config |
80
+
81
+ > **Important:** both volumes are required. The token volume (`finance-config`) ensures your bearer token survives restarts — without it, a new token is generated on every start and clients lose access.
82
+
83
+ ## Retrieving the bearer token
84
+
85
+ The service auto-generates a bearer token on first start and writes it to `/root/.pi/sf/finance/token` inside the container (persisted via the `finance-config` volume). Retrieve it with:
86
+
87
+ ```bash
88
+ docker compose exec finance-api cat /root/.pi/sf/finance/token
89
+ ```
90
+
91
+ Use this token for all authenticated API requests:
92
+
93
+ ```bash
94
+ curl -H "Authorization: Bearer <token>" http://127.0.0.1:7780/v1/holdings
95
+ ```
96
+
97
+ The `@pi-stef/finance` extension client reads this token automatically when both run on the same host. In Docker, copy the token into the extension's config (`~/.pi/sf/finance/config.json`) or the `SF_FINANCE_TOKEN` env var.
98
+
99
+ ## Healthcheck
100
+
101
+ The container includes a built-in healthcheck hitting `/v1/health` every 30s:
102
+
103
+ ```bash
104
+ docker compose ps # STATUS column shows "healthy"
105
+ ```
106
+
107
+ ## GHCR visibility
108
+
109
+ The first push creates the package under the `sfiorini` namespace on GHCR. By default the image inherits the repository's visibility (private for a private repo). To allow unauthenticated pulls, set the package to **public** in GitHub → Packages → `pi-stef/finance-api` → Package settings.
110
+
111
+ ## Troubleshooting
112
+
113
+ | Symptom | Fix |
114
+ |---------|-----|
115
+ | `401 Unauthorized` | Token mismatch — retrieve it from the container (above) and update client config |
116
+ | Port already in use | Change `SF_FINANCE_PORT` and the compose port mapping |
117
+ | `better-sqlite3` build fails | Use the prebuilt registry image; building from source requires the build stage's toolchain |
118
+ | Can't reach service from another container | Use the service name `finance-api` as the hostname, or set `SF_FINANCE_HOST=0.0.0.0` and join the same Docker network |
119
+ | Healthcheck never goes healthy | Check `docker compose logs finance-api`; ensure the DB volume is writable |
120
+
121
+ ## Native (non-Docker) alternative
122
+
123
+ See [docs/native-run.md](../docs/native-run.md) for launchd/systemd setup without Docker.
@@ -1,10 +1,12 @@
1
- version: "3.8"
2
-
3
1
  services:
4
2
  finance-api:
5
- build:
6
- context: ../.. # Repo root
7
- dockerfile: packages/finance-api/docker/Dockerfile
3
+ # Pull the published image from GHCR (default):
4
+ image: ghcr.io/sfiorini/pi-stef/finance-api:latest
5
+ # To build from local source instead, comment out `image:` above and run:
6
+ # docker compose up --build
7
+ # build:
8
+ # context: ../..
9
+ # dockerfile: packages/finance-api/docker/Dockerfile
8
10
  ports:
9
11
  - "127.0.0.1:7780:7780" # Localhost only for security
10
12
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-stef/finance-api",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Always-on local service for @pi-stef/finance: ingests financial-account data, stores locally, runs a deterministic quant engine and market-aware scheduler.",
5
5
  "type": "module",
6
6
  "license": "MIT",