Skip to content

Commit c82577d

Browse files
committed
docs(readme): add dotenvx encryption/decryption guide for production env management\n\n- Explain dotenvx workflow for encrypting production .env files\n- Document DOTENV_PRIVATE_KEY requirement and how to provide it\n- Add docker and compose examples for dotenvx runtime\n- Update Requirements section to include dotenvx for production\n
1 parent 40c75e1 commit c82577d

File tree

1 file changed

+263
-1
lines changed

1 file changed

+263
-1
lines changed

README.md

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,263 @@
1-
# budget-api
1+
# Budget Automation API (Actual Budget wrapper)
2+
3+
A secure Node.js/Express REST API that wraps the Actual Budget SDK (`@actual-app/api`). It provides JWT-based auth, optional OAuth2 for n8n, Swagger documentation, and a hardened runtime (helmet, CORS, structured logging, rate limits per route).
4+
5+
## Features
6+
- Authentication: JWT access/refresh tokens, session login for docs
7+
- Optional OAuth2: first-party flow for n8n (`/oauth/authorize`, `/oauth/token`)
8+
- Endpoints: accounts, transactions, budgets, categories, payees, rules, schedules, query
9+
- API Docs: protected Swagger UI at `/docs` with OpenAPI source in [src/docs/openapi.yml](src/docs/openapi.yml)
10+
- Security: helmet headers, request IDs, SQLite token revocation, minimal defaults
11+
- Docker: production image + dev `docker compose` stack (Actual Server + n8n)
12+
13+
## Requirements
14+
- Node.js 22+ and npm
15+
- Actual Budget Server credentials (or use the dev `docker compose` stack)
16+
- For OAuth2 to n8n (optional): n8n instance and client credentials
17+
- For production: dotenvx CLI and encrypted `.env` file with `DOTENV_PRIVATE_KEY`
18+
19+
## Development
20+
`npm run dev` works locally when Actual Server is running and all required env vars are set. Choose one approach below.
21+
22+
### Option 1: Docker Dev Stack (Full-featured, Simplest for Local Testing)
23+
Bring up all services (API + Actual Server + n8n) with automatic rebuilds on code changes:
24+
25+
```bash
26+
docker compose -f docker-compose.dev.yml up --build --force-recreate --remove-orphans
27+
```
28+
29+
First-run initialization (Actual Server):
30+
- On first startup, Actual Server is not configured.
31+
- Open http://localhost:5006 and set a password.
32+
- Create/open your budget and obtain the Sync ID:
33+
- In the Actual app, go to Settings → Advanced → Show Sync ID (or similar).
34+
- Add the following to `.env.local` (mounted as `/app/.env` in the API container):
35+
36+
```
37+
ADMIN_PW=ChangeMe_very_strong!
38+
JWT_SECRET=replace-with-64b-random
39+
JWT_REFRESH_SECRET=replace-with-64b-random
40+
41+
ACTUAL_SERVER_URL=http://actual-server-dev:5006
42+
ACTUAL_PASSWORD=<the password you set in Actual Server>
43+
ACTUAL_SYNC_ID=<your budget sync id>
44+
45+
# Optional n8n OAuth2
46+
N8N_CLIENT_ID=example-n8n
47+
N8N_CLIENT_SECRET=replace-with-long-secret
48+
N8N_OAUTH2_CALLBACK_URL=http://localhost:5678/rest/oauth2-credential/callback
49+
```
50+
51+
Then restart the API container (or re-run the compose command) so it picks up `.env.local`.
52+
53+
Ports:
54+
- API: http://localhost:3000
55+
- n8n: http://localhost:5678
56+
- Actual Server: http://localhost:5006
57+
58+
### Option 2: Host Dev with Docker Actual Server (Hot Reload, Faster Iteration)
59+
Run Actual Server in Docker; run the API on your host with live reload (no rebuild needed):
60+
61+
1. Start Actual Server only:
62+
63+
```bash
64+
docker compose -f docker-compose.dev.yml up -d actual-server
65+
```
66+
67+
2. Set env vars locally and run the API:
68+
69+
```bash
70+
export ADMIN_PW=ChangeMe_very_strong!
71+
export JWT_SECRET=replace-with-64b-random
72+
export JWT_REFRESH_SECRET=replace-with-64b-random
73+
export ACTUAL_SERVER_URL=http://localhost:5006
74+
export ACTUAL_PASSWORD=<actual password>
75+
export ACTUAL_SYNC_ID=<budget sync id>
76+
export ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5678
77+
npm ci
78+
npm run dev
79+
```
80+
81+
Benefits: code changes reload instantly via Node 22 `--watch`; no rebuilds needed.
82+
83+
## Production
84+
Set the same Actual credentials via environment variables in `.env` (not `.env.local`). The production Actual Server provides the password and sync id values. For n8n in production:
85+
- Configure the same OAuth2 endpoint and credentials
86+
- Ensure `ALLOWED_ORIGINS` includes your n8n instance's URL
87+
- Use HTTPS for the callback URL
88+
89+
### Environment Management with dotenvx
90+
This project uses **dotenvx** to manage encrypted environment files. In production, a `.env` file (or encrypted `.env.prod`) is required along with the decryption key.
91+
92+
1. Create a `.env` file with production values or use dotenvx to encrypt it:
93+
94+
```bash
95+
# Initialize dotenvx (generates .env.keys)
96+
dotenvx new
97+
98+
# Encrypt your production .env
99+
dotenvx set ADMIN_PW "your-production-password"
100+
dotenvx set JWT_SECRET "your-production-jwt-secret"
101+
# ... repeat for other required vars
102+
```
103+
104+
2. Securely store the private key:
105+
- The key is in `.env.keys` (never commit this)
106+
- Provide `DOTENV_PRIVATE_KEY` to the runtime (e.g., as a secret in CI/CD or container env)
107+
- The Docker container reads `DOTENV_PRIVATE_KEY` from the environment or compose file
108+
109+
3. Run with dotenvx:
110+
111+
```bash
112+
# With the private key set:
113+
export DOTENV_PRIVATE_KEY="$(grep DOTENV_PRIVATE_KEY .env.keys | cut -d '=' -f2 | tail -n1)"
114+
115+
# Or in Docker:
116+
docker run \
117+
-v ./data/actual-api:/app/.actual-cache \
118+
-p 3000:3000 \
119+
actual-api-wrapper:latest
120+
121+
# Or with compose:
122+
docker compose up -d --force-recreate --build
123+
```
124+
125+
See the [docker-compose.yml](docker-compose.yml) for how it's wired up in containers.
126+
127+
## Environment Variables
128+
- ADMIN_USER: admin username (default `admin`)
129+
- ADMIN_PW: required; admin password (validated for complexity)
130+
- SESSION_SECRET: required in production (random in dev if omitted)
131+
- JWT_SECRET: required; HMAC secret for access tokens
132+
- JWT_REFRESH_SECRET: required; HMAC secret for refresh tokens
133+
- JWT_ACCESS_TTL: optional, default `1h` (supports `30m`, `3600`, etc.)
134+
- JWT_REFRESH_TTL: optional, default `24h`
135+
- PORT: server port (default `3000`)
136+
- ALLOWED_ORIGINS: CSV of allowed origins for CORS
137+
- TRUST_PROXY: set `true` if running behind a reverse proxy
138+
- LOG_LEVEL: winston log level (default `info`)
139+
- DATA_DIR: Actual data directory (default `/app/.actual-cache`); stores `auth.db`
140+
- ACTUAL_SERVER_URL: Actual server URL (e.g., `http://localhost:5006`)
141+
- ACTUAL_PASSWORD: Actual server password
142+
- ACTUAL_SYNC_ID: Budget sync ID
143+
- N8N_CLIENT_ID / N8N_CLIENT_SECRET / N8N_OAUTH2_CALLBACK_URL: enable OAuth2 endpoints when all are present
144+
145+
## API Docs & Validation
146+
- OpenAPI source: [src/docs/openapi.yml](src/docs/openapi.yml)
147+
- Local docs (auth required): GET `/docs`
148+
- Validate OpenAPI:
149+
150+
```bash
151+
npm run validate:openapi
152+
```
153+
154+
## Auth Flows
155+
- Local login (session for docs):
156+
- GET `/login` → render form
157+
- POST `/login` → create session, then access `/docs`
158+
- JWT login:
159+
- POST `/auth/login` with `{ "username": "admin", "password": "..." }`
160+
- Response contains `access_token`, `refresh_token`, `expires_in`
161+
- Send `Authorization: Bearer <access_token>` to protected routes
162+
- n8n OAuth2 (optional):
163+
- Configure env vars listed above
164+
- Endpoints available: `/oauth/authorize`, `/oauth/token`
165+
- See [Connecting n8n](#connecting-n8n) for setup details.
166+
167+
## Connecting n8n
168+
n8n can integrate with the API via OAuth2 for secure token-based workflows. Use either built-in session/JWT auth or the OAuth2 flow.
169+
170+
### Option 1: Basic Auth or Bearer Token (Quick Start)
171+
For development, you can use manual session login or JWT bearer tokens:
172+
173+
1. Log in via basic auth:
174+
- Enter credentials in n8n HTTP node with basic auth.
175+
176+
2. Or, obtain a bearer token:
177+
- POST to `/auth/login` with `{ "username": "admin", "password": "..." }`
178+
- Copy the `access_token` from the response
179+
- In n8n, create a credential of type "Generic Credential Type" or similar HTTP auth
180+
- Set header: `Authorization: Bearer <access_token>`
181+
182+
### Option 2: OAuth2 Flow (Production Recommended)
183+
Set up OAuth2 for secure, refreshable tokens:
184+
185+
1. Configure env vars (already in `.env.local` example above):
186+
- `N8N_CLIENT_ID`: a unique identifier (e.g., `example-n8n`)
187+
- `N8N_CLIENT_SECRET`: a long random secret (32+ chars)
188+
- `N8N_OAUTH2_CALLBACK_URL`: n8n's OAuth callback URL (e.g., `http://localhost:5678/rest/oauth2-credential/callback`)
189+
190+
2. In n8n, add a new credential:
191+
- Select **OAuth2** type
192+
- **Authorization URL**: `http://localhost:3000/oauth/authorize`
193+
- **Token URL**: `http://actual-api-wrapper-dev:3000/oauth/token`
194+
- **Client ID**: same as `N8N_CLIENT_ID`
195+
- **Client Secret**: same as `N8N_CLIENT_SECRET`
196+
- **Redirect URL**: same as `N8N_OAUTH2_CALLBACK_URL`
197+
- Authorize and use in n8n workflows
198+
199+
3. Test in n8n:
200+
- Add an HTTP request node
201+
- Set URL to an API endpoint (e.g., `http://actual-api-wrapper-dev:3000/accounts`)
202+
- In authentication, select the OAuth2 credential you just created
203+
- Execute the node
204+
205+
Benefits of OAuth2:
206+
- Tokens are refreshed automatically
207+
- No passwords are stored in n8n
208+
- Tokens can be revoked from the API
209+
210+
### Troubleshooting
211+
- **"Resource not accessible" in n8n**: Check that the API and n8n are on the same network (both in docker-compose).
212+
- **Token expired**: OAuth2 automatically refreshes; session tokens may need manual re-login.
213+
- **CORS error**: Verify `ALLOWED_ORIGINS` includes n8n's origin (e.g., `http://localhost:5678`).
214+
215+
## CLI Commands
216+
- Lint: `npm run lint`
217+
- Audit: `npm run audit`
218+
- Pre-commit hooks: see [PRECOMMIT_SETUP.md](PRECOMMIT_SETUP.md)
219+
220+
## Docker (Production)
221+
Build and run the image:
222+
223+
```bash
224+
docker build -t actual-api-wrapper:latest .
225+
docker run --rm -p 3000:3000 \
226+
-v $(pwd)/data/actual-api:/app/.actual-cache \
227+
--env-file ./.env \
228+
actual-api-wrapper:latest
229+
```
230+
231+
Or use the compose file:
232+
233+
```bash
234+
docker compose up -d --build
235+
```
236+
237+
## Data & Persistence
238+
- SQLite auth DB: `${DATA_DIR}/auth.db` (persist the `DATA_DIR` volume)
239+
- Actual SDK cache and budget data are managed by `@actual-app/api` using `DATA_DIR`
240+
241+
## Observability
242+
- Structured logs via winston (JSON in production), respect `LOG_LEVEL` and `NODE_ENV`
243+
- Each request includes an `X-Request-ID` for traceability
244+
245+
## CI / Security
246+
GitHub Actions run dependency and image security checks:
247+
- npm audit, ESLint, Docker build test
248+
- Snyk (requires `SNYK_TOKEN` secret)
249+
- Container scan via Trivy (SARIF uploaded to code scanning)
250+
- Secret scanning via Gitleaks
251+
- OWASP Dependency-Check (SARIF upload)
252+
253+
Workflow tips:
254+
- SARIF uploads require `permissions: { security-events: write, actions: read }`
255+
- Forked PRs skip uploads to avoid permission errors
256+
257+
## Project Structure
258+
- App: [src](src)
259+
- Routes: [src/routes](src/routes)
260+
- Auth: [src/auth](src/auth)
261+
- Config: [src/config](src/config)
262+
- Docs: [src/docs](src/docs)
263+
- Logging: [src/logging](src/logging)

0 commit comments

Comments
 (0)