A secure Node.js/Express REST API that wraps the Actual Budget SDK (@actual-app/api). It provides JWT-based auth with role-based access control, optional OAuth2 for n8n, admin API for OAuth client management, PostgreSQL or SQLite database support, Swagger documentation, and a hardened runtime (helmet, CORS, structured logging, rate limits per route).
# Create an Account
## Get Token
TOKEN=$(
curl http://localhost:3000/v2/auth/login \
-H "Content-Type: application/json" \
-X POST \
-d '{"username":"admin","password":"admin"}' \
-s | jq -r '.access_token' \
)
## Get Accounts
curl http://localhost:3000/v2/accounts \
-H "Authorization: Bearer $TOKEN"
## Create 'test' Account
curl http://localhost:3000/v2/accounts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"account":{"name":"test","offbudget":true,"closed":true},"initialBalance":500}'
## Get Accounts, showing 'test'
curl http://localhost:3000/v2/accounts \
-H "Authorization: Bearer $TOKEN"
- Authentication: JWT access/refresh tokens, session login for docs, role-based access control (RBAC)
- Optional OAuth2: first-party flow for n8n (
/oauth/authorize,/oauth/token) - Admin API: OAuth client management endpoints (
/admin/oauth-clients) with secure secret hashing - Endpoints: accounts, transactions, budgets, categories, payees, rules, schedules, query
- API Docs: protected Swagger UI at
/docswith OpenAPI source in src/docs/openapi.yml - Database Support: PostgreSQL (recommended for production) or SQLite (default, simpler setup)
- Security: helmet headers, request IDs, token revocation, rate limiting, input validation, bcrypt-hashed OAuth secrets
- Environment Validation: Automatic validation of all environment variables on startup
- Metrics: Built-in Prometheus metrics collection at
/v2/metrics/prometheusendpoint - Monitoring: Pre-configured Prometheus and Grafana setup for real-time metrics visualization (see monitoring/)
- Health Checks: Comprehensive health endpoint with database and API connectivity checks
- Redis Support: Optional Redis for distributed rate limiting (falls back to memory)
- Docker: production image + dev
docker composestack (Actual Server + n8n + Redis + Prometheus + Grafana)
- Node.js 22+ and npm
- Docker and Docker Compose (for recommended development workflow)
- Actual Budget Server credentials (or use the dev
docker composestack) - For OAuth2 to n8n (optional): n8n instance and client credentials
- For production: Secrets manager (GitHub Secrets, AWS Secrets Manager, etc.) for secure environment variable management
This section covers production deployment. For development setup, see the Development section below.
-
Clone the repository with submodules:
git clone --recurse-submodules https://github.com/ZoneMix/actual-budget-rest-api.git cd actual-budget-rest-apiImportant: The
--recurse-submodulesflag is required because this project includes then8n-nodes-actual-budget-rest-apias a git submodule. If you've already cloned without it, run:git submodule update --init --recursive
-
Docker and Docker Compose (for containerized deployment):
- Docker 20.10+ and Docker Compose 2.0+
- Or use the production Docker image directly
Create a .env file with the following minimum required variables for production:
# Application environment
NODE_ENV=production
# Admin credentials
ADMIN_USER=admin
ADMIN_PASSWORD=YourSecurePassword123! # Must meet complexity requirements (12+ chars, uppercase, lowercase, number, special char)
# JWT secrets (MUST be 32+ characters in production)
JWT_SECRET=your-jwt-secret-at-least-32-characters-long
JWT_REFRESH_SECRET=your-refresh-secret-different-from-jwt-secret
SESSION_SECRET=your-session-secret-different-from-jwt-secrets
# Actual Budget Server connection
ACTUAL_SERVER_URL=https://your-actual-server.com # Your production Actual Server URL
ACTUAL_PASSWORD=your-actual-server-password
ACTUAL_SYNC_ID=your-budget-sync-idGenerate secure secrets:
# Generate secure secrets (32+ characters) - use different values for each!
openssl rand -base64 32 # For JWT_SECRET
openssl rand -base64 32 # For JWT_REFRESH_SECRET (must be different!)
openssl rand -base64 32 # For SESSION_SECRET (must be different!)Security Note: In production, all secrets must be:
- At least 32 characters long
- Unique (never reuse the same secret for different purposes)
- Randomly generated (use
openssl rand -base64 32)
See .env.example for a complete list of all available environment variables with descriptions.
For production deployments, use a secrets manager to securely manage environment variables. This is the recommended approach for CI/CD pipelines, Kubernetes, and cloud deployments.
-
Store secrets in GitHub:
- Go to your repository → Settings → Secrets and variables → Actions
- Add each environment variable as a secret (e.g.,
ADMIN_PASSWORD,JWT_SECRET, etc.)
-
Use in GitHub Actions workflow:
- name: Deploy to production env: ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} JWT_SECRET: ${{ secrets.JWT_SECRET }} JWT_REFRESH_SECRET: ${{ secrets.JWT_REFRESH_SECRET }} # ... other secrets run: docker compose up -d --build
-
Store secrets in AWS:
aws secretsmanager create-secret \ --name actual-rest-api/admin-password \ --secret-string "YourSecurePassword123!" -
Retrieve and inject in deployment:
export ADMIN_PASSWORD=$(aws secretsmanager get-secret-value \ --secret-id actual-rest-api/admin-password \ --query SecretString --output text)
-
Create secrets:
kubectl create secret generic actual-rest-api-secrets \ --from-literal=ADMIN_PASSWORD='YourSecurePassword123!' \ --from-literal=JWT_SECRET='your-jwt-secret' \ # ... other secrets
-
Reference in deployment:
env: - name: ADMIN_PASSWORD valueFrom: secretKeyRef: name: actual-rest-api-secrets key: ADMIN_PASSWORD
For local development, you can use a .env file:
cp .env.example .env
# Edit .env with your values
docker compose up -d --build.env files to git. Always use secrets managers in production.
Docker Compose with PostgreSQL (recommended for production):
# Set environment variables via secrets manager or .env file
# Required: DB_TYPE=postgres and PostgreSQL connection parameters
# POSTGRES_URL=postgresql://user:password@postgres:5432/database
# OR use individual parameters: POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD
docker compose -f docker-compose.prod.postgres.yml up -d --buildDocker Compose with SQLite (simpler, single-container):
# Set environment variables via secrets manager or .env file
# Required: DB_TYPE=sqlite
docker compose -f docker-compose.prod.sqlite.yml up -d --buildNote: For production, inject environment variables from your secrets manager (GitHub Secrets, AWS Secrets Manager, etc.) rather than using .env files.
Docker Image:
docker build -t actual-rest-api:latest .
docker run -d \
--name actual-rest-api \
-e ADMIN_PASSWORD="$ADMIN_PASSWORD" \
-e JWT_SECRET="$JWT_SECRET" \
-e JWT_REFRESH_SECRET="$JWT_REFRESH_SECRET" \
-e DB_TYPE=postgres \
-e POSTGRES_URL="postgresql://user:password@host:5432/database" \
# ... add all other required environment variables from secrets manager
-v $(pwd)/data/actual-api:/app/.actual-cache \
-p 3000:3000 \
actual-rest-api:latestNote: In production, retrieve secrets from your secrets manager and pass them as environment variables. Never hardcode secrets in scripts or commit them to version control.
Production Checklist:
- ✅ Use secrets manager (GitHub Secrets, AWS Secrets Manager, etc.) for all sensitive environment variables
- ✅ Use HTTPS with reverse proxy (nginx, Traefik, etc.)
- ✅ Set
TRUST_PROXY=trueif behind reverse proxy - ✅ Configure
ALLOWED_ORIGINSwith production domains - ✅ Set
LOG_LEVEL=warnorerrorfor production - ✅ Configure Redis for distributed rate limiting
- ✅ Set up monitoring (Prometheus/Grafana) - see monitoring/
- ✅ Regular backups of
DATA_DIRvolume - ✅ For n8n: Use HTTPS callback URLs, configure OAuth2 credentials
- ✅ Never commit
.envfiles or secrets to version control
-
Setup environment:
cp .env.example .env.local # Edit .env.local with your values (see below for minimum requirements) -
Start all services:
docker compose -f docker-compose.dev.yml up --build
-
Configure Actual Server (first run only):
- Open http://localhost:5006 → Set password → Create/open budget
- Get Sync ID from Settings → Advanced → Show Sync ID
- Update
ACTUAL_PASSWORDandACTUAL_SYNC_IDin.env.local - Restart:
docker compose -f docker-compose.dev.yml restart actual-rest-api-dev
-
Access services:
- API: http://localhost:3000
- Actual Server: http://localhost:5006
- n8n: http://localhost:5678
- Grafana: http://localhost:3001 (admin/admin)
- Prometheus: http://localhost:9090
Minimum .env.local for development:
ADMIN_USER=admin
ADMIN_PASSWORD=Password123!
JWT_SECRET=dev-secret-not-for-production
JWT_REFRESH_SECRET=dev-refresh-secret-not-for-production
ACTUAL_SERVER_URL=http://actual-server-dev:5006
ACTUAL_PASSWORD=<your-actual-server-password>
ACTUAL_SYNC_ID=<your-budget-sync-id>Note: In development, missing secrets are auto-generated with warnings. Secrets can be shorter than production requirements.
The dev stack includes Prometheus and Grafana. Access Grafana at http://localhost:3001 (admin/admin) → Dashboards → Actual Budget REST API Metrics.
The dashboard shows request rates, error rates, response times, and more. See monitoring/README.md for configuration details.
All variables are validated on startup. Invalid or missing required variables cause the application to exit with clear error messages.
ADMIN_USER: Admin username (default:admin)ADMIN_PASSWORD: Admin password (12+ chars, complexity required)JWT_SECRET: JWT signing key (32+ chars in production)JWT_REFRESH_SECRET: Refresh token key (32+ chars, different fromJWT_SECRET)SESSION_SECRET: Session encryption key (32+ chars, different from JWT secrets)ACTUAL_SERVER_URL: Actual Budget Server URLACTUAL_PASSWORD: Actual Budget Server passwordACTUAL_SYNC_ID: Budget sync ID
PORT: Server port (default:3000)NODE_ENV: Environment mode (development|production|test)JWT_ACCESS_TTL: Access token TTL (default:1h)JWT_REFRESH_TTL: Refresh token TTL (default:24h)ALLOWED_ORIGINS: CORS origins (CSV, default:http://localhost:3000,http://localhost:5678)TRUST_PROXY: Trust proxy headers (default:false)LOG_LEVEL: Log level (default:info)DATA_DIR: Data directory (default:/app/.actual-cache)REDIS_URL/REDIS_HOST/REDIS_PORT/REDIS_PASSWORD: Redis connectionN8N_CLIENT_ID/N8N_CLIENT_SECRET/N8N_OAUTH2_CALLBACK_URL: OAuth2 for n8nENABLE_CORS/ENABLE_HELMET/ENABLE_RATE_LIMITING: Feature toggles (default:true)MAX_REQUEST_SIZE: Max request body size (default:10kb)DB_TYPE: Database type (sqlite|postgres, default:postgres)POSTGRES_URL: PostgreSQL connection URL (format:postgresql://user:password@host:port/database)POSTGRES_HOST/POSTGRES_PORT/POSTGRES_DB/POSTGRES_USER/POSTGRES_PASSWORD: PostgreSQL connection details (alternative toPOSTGRES_URL)
Development Mode: In NODE_ENV=development, secrets can be shorter and missing secrets are auto-generated with warnings.
See .env.example for complete reference.
- OpenAPI source: src/docs/openapi.yml
- Local docs (auth required): GET
/docs - Validate OpenAPI:
npm run validate:openapi- Local login (session for docs):
- GET
/docs→ redirect to/login - POST
/login→ create session, then access/docs
- GET
- JWT login:
- POST
/v2/auth/loginwith{ "username": "admin", "password": "..." } - Response contains
access_token,refresh_token,expires_in,scope,token_type - Tokens include user
roleandscopesfor authorization - Send
Authorization: Bearer <access_token>to protected routes - Rate limited: 5 requests per 15 minutes
- POST
- JWT logout:
- POST
/v2/auth/logoutwith optionalrefresh_tokenin body - Revokes both access and refresh tokens for secure session termination
- POST
- n8n OAuth2 (optional):
- Configure env vars listed above
- Endpoints available:
/oauth/authorize,/oauth/token - Client secrets are hashed with bcrypt before storage
- See Connecting n8n for setup details.
- Admin API (requires admin role):
- Access admin dashboard at
/admin(HTML interface) - Manage OAuth clients via
/admin/oauth-clientsendpoints - Requires JWT token with
adminrole andadminscope
- Access admin dashboard at
The /v2/query endpoint allows executing ActualQL queries against Actual Budget data:
- Security: Table whitelist, filter depth limits, result size limits
- Rate Limited: 20 requests per minute
- Audit Logging: All queries logged with user ID and request context
- Documentation: See ActualQL docs
-
Configure environment variables:
N8N_CLIENT_ID=example-n8n N8N_CLIENT_SECRET=<32+ character secret> N8N_OAUTH2_CALLBACK_URL=http://localhost:5678/rest/oauth2-credential/callback
-
In n8n, create OAuth2 credential:
- Type: OAuth2
- Authorization URL:
http://localhost:3000/oauth/authorize(or your API URL) - Token URL:
http://actual-rest-api-dev:3000/oauth/token(use Docker service name) - Client ID & Secret: Match your env vars
- Redirect URL: Match
N8N_OAUTH2_CALLBACK_URL
-
Use in workflows: Select the OAuth2 credential in HTTP request nodes.
Benefits: Automatic token refresh, no passwords stored, revocable tokens.
For development, use JWT bearer tokens:
- POST to
/v2/auth/login→ Getaccess_token - In n8n HTTP node, set header:
Authorization: Bearer <token>
Note: In production behind a reverse proxy, replace localhost and Docker hostnames with actual domains.
The Admin API provides endpoints for managing OAuth clients. All endpoints require authentication with an admin role.
- Web Interface: Navigate to
/adminin your browser (requires admin session login) - API Endpoints: Use JWT tokens with
adminrole andadminscope
GET /admin/oauth-clients- List all OAuth clients (without secrets)POST /admin/oauth-clients- Create a new OAuth client (auto-generates secret if not provided)GET /admin/oauth-clients/:clientId- Get a specific OAuth clientPUT /admin/oauth-clients/:clientId- Update an OAuth client (secret, scopes, redirect URIs)DELETE /admin/oauth-clients/:clientId- Delete an OAuth client
# Get admin token
TOKEN=$(curl http://localhost:3000/v2/auth/login \
-H "Content-Type: application/json" \
-X POST \
-d '{"username":"admin","password":"admin"}' \
-s | jq -r '.access_token')
# Create a new OAuth client
curl http://localhost:3000/admin/oauth-clients \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-X POST \
-d '{
"client_id": "my-app",
"allowed_scopes": "api",
"redirect_uris": "http://localhost:8080/callback"
}'Note: The client_secret is only returned once on creation - save it immediately! All secrets are hashed with bcrypt before storage.
# Testing & Quality
npm test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
npm run lint # Lint code
npm run audit # Security audit
npm run validate:openapi # Validate OpenAPI spec
# Docker Development
docker compose -f docker-compose.dev.yml up --build
docker compose -f docker-compose.dev.yml logs -f actual-rest-api-devSee PRECOMMIT_SETUP.md for pre-commit hooks setup.
- Database Options:
- PostgreSQL (recommended for production): Set
DB_TYPE=postgresand configurePOSTGRES_URLor individual connection parameters - SQLite (default, simpler setup): Set
DB_TYPE=sqlite, database stored at${DATA_DIR}/auth.db
- PostgreSQL (recommended for production): Set
- Automatic Migrations: Schema migrations run on startup (adds
role,scopes,updated_atcolumns to users table,client_secret_hashedto clients table) - User Roles & Scopes: Users have
role(e.g.,admin,user) andscopes(comma-separated, e.g.,api,admin) for authorization - OAuth Client Secrets: All client secrets are hashed with bcrypt before storage for security
- Actual SDK cache and budget data are managed by
@actual-app/apiusingDATA_DIR
- Logging: Structured JSON logs (winston), respects
LOG_LEVEL. Each request includesX-Request-IDfor tracing. - Metrics: Prometheus endpoint at
/metrics/prometheus. Pre-configured Grafana dashboards in monitoring/. - Health:
/healthendpoint returns 200 (healthy) or 503 (degraded). Checks database, Actual API, and system resources.
GitHub Actions run dependency and image security checks:
- npm audit, ESLint, Docker build test
- Snyk (requires
SNYK_TOKENsecret) - Container scan via Trivy (SARIF uploaded to code scanning)
- Secret scanning via Gitleaks
- OWASP Dependency-Check (SARIF upload)
Workflow tips:
- SARIF uploads require
permissions: { security-events: write, actions: read } - Forked PRs skip uploads to avoid permission errors
- App: src
- Routes: src/routes
- Auth: src/auth
- Config: src/config - includes environment validation
- Docs: src/docs
- Logging: src/logging
- Errors: src/errors - custom error classes
- Middleware: src/middleware - rate limiting, validation, metrics, etc.
- Tests: tests - Jest test suite
- ARCHITECTURE.md - System architecture and design patterns
- SECURITY.md - Security model and threat analysis
- .env.example - Complete environment variable reference


