-
Start PostgreSQL and set environment variables (defaults in
application.properties):DATABASE_URL(JDBC URL; put credentials in query params, notuser:pass@host)- Example:
jdbc:postgresql://<host>/<db>?user=<user>&password=<url-encoded>&sslmode=require&channel_binding=require
- Example:
-
Use
application-local.propertiesfor local overrides (profilelocal), andapplication.propertiesfor shared defaults. Example local file:spring.datasource.url=jdbc:postgresql://localhost:5432/transactions app.security.dev-user-id=local-user
Run with
SPRING_PROFILES_ACTIVE=localto pick it up. -
Run migrations and start the app:
mvn -B spring-boot:run
GET /api/v1/health— basic health check
GET /api/v1/trades— list trades for the callerGET /api/v1/trades/paged— paginated trades with optional month filterPOST /api/v1/trades— create a trade (stocks or options, long or short)PUT /api/v1/trades/{id}— update a trade (must belong to caller)DELETE /api/v1/trades/{id}— remove a tradeGET /api/v1/trades/summary— realized P/L totals with daily and monthly buckets (optionally filtered by month)GET /api/v1/trades/stats— aggregate statistics (total P/L, trade count, best day, best month) with CAD to USD conversionGET /api/v1/trades/share/{token}— view shared trade by token
GET /api/v1/admin/users— list users
Trade fields are intentionally minimal: symbol, asset type (stock/option), currency (USD/CAD), direction (long/short), quantity, entry/exit prices, fees, open/close dates, notes, and option-specific details (type/strike/expiry). Realized P/L is calculated server-side on create/update.
Run all tests:
mvn testRun specific test class:
mvn test -Dtest=TradeServiceTest- Default: stateless requests,
X-User-Idheader is accepted and turned into an authenticated principal - Set
app.security.dev-user-id=local-userto avoid passing the header locally - Health endpoint is open (
/api/v1/healthand/); all other endpoints require authentication
Set the following properties:
app.security.jwt.enabled=trueapp.security.jwt.issuer-uri=https://accounts.google.comapp.security.jwt.audience=<Google client id>app.security.allow-header-auth=false
Spring Security validates bearer tokens and uses the JWT sub (or email) as the caller id.
app.security.admin-emails(comma-separated list)- If unset, the admin list falls back to
app.security.allowed-emails
Flyway runs migrations from src/main/resources/db/migration:
V1__trades.sql— creates the trades tableV2__share_tokens.sql— adds share token functionalityV3__add_currency.sql— adds currency field for CAD/USD supportV4__optimize_aggregate_queries.sql— adds performance indexes for aggregate stats
Required:
DATABASE_URL(JDBC URL with credentials in query params)APP_CORS_ALLOWED_ORIGINSAPP_SECURITY_JWT_ENABLED=trueAPP_SECURITY_ALLOW_HEADER_AUTH=falseAPP_SECURITY_JWT_ISSUER_URI=https://accounts.google.comAPP_SECURITY_JWT_AUDIENCE=<Google client id>
Optional:
APP_SECURITY_ALLOWED_EMAILS(comma-separated allowlist)APP_SECURITY_ADMIN_EMAILS(comma-separated admin allowlist)APP_SECURITY_JWT_JWK_SET(pin JWKS JSON)
CI runs on push/PR. Dev deploys automatically on main after tests pass. Prod deploys are manual via workflow_dispatch. Deploys update the App Runner service after pushing a new ECR image.
AWS_REGIONAWS_ROLE_ARNDEV_ECR_TRANSACTION_API_REPODEV_BACKEND_SERVICE_ARN(App Runner service ARN)DEV_DATABASE_URLDEV_CORS_ALLOWED_ORIGINSDEV_ALLOWED_EMAILS(optional)DEV_ADMIN_EMAILS(optional)DEV_GOOGLE_CLIENT_IDDEV_GOOGLE_JWK_SET(optional)
AWS_REGIONAWS_ROLE_ARNPROD_ECR_TRANSACTION_API_REPOPROD_BACKEND_SERVICE_ARN(App Runner service ARN)PROD_DATABASE_URLPROD_CORS_ALLOWED_ORIGINSPROD_GOOGLE_CLIENT_IDPROD_GOOGLE_JWK_SET(optional)PROD_ADMIN_EMAILS(optional)
Role permissions for App Runner deploys must include:
apprunner:UpdateServiceapprunner:DescribeService
- Goal: keep App Runner inside a VPC to reach Neon/RDS without requiring outbound internet/NAT for BoC.
- Flow: Lambda (outside VPC) fetches BoC/CBSA FX, writes latest CADUSD into DynamoDB.
- App Runner: reads the latest CAD/USD rate from DynamoDB on startup + daily schedule.
- Local: uses the direct BoC/CBSA HTTP call for quick debugging.
- Why DynamoDB: stable, low-cost, VPC-friendly via a DynamoDB gateway endpoint.
- Aggregate Stats Endpoint (
/api/v1/trades/stats) uses database-level aggregation with native SQL queries for O(1) memory usage - CAD to USD Conversion performed in SQL queries using CASE expressions
- Composite Indexes on
(user_id, closed_at)and(user_id, currency, closed_at)for fast aggregate queries - Pagination support for large trade lists
CREATE TABLE trades (
id UUID PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
symbol VARCHAR(10) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
asset_type VARCHAR(10) NOT NULL,
direction VARCHAR(5) NOT NULL,
quantity INTEGER NOT NULL,
entry_price NUMERIC(12,2) NOT NULL,
exit_price NUMERIC(12,2) NOT NULL,
fees NUMERIC(10,2) DEFAULT 0,
realized_pnl NUMERIC(12,2),
opened_at DATE NOT NULL,
closed_at DATE NOT NULL,
option_type VARCHAR(4),
strike_price NUMERIC(12,2),
expiry_date DATE,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);The companion frontend lives at: https://github.com/alexmcdermid/tradingView