Multiuser And Workspaces

Use this guide when you want to inspect the current session, create a workspace, or call workspace-scoped paper trading and backtest endpoints.

The app has an early multiuser/workspace API layer. It is still local-first: authentication defaults to a local user, and app-created state is stored in a local SQLite app-state database while the PostgreSQL-backed version is being built.

Quick Start

  1. Start the backend and frontend.
  2. Check the current session with GET /api/v1/auth/session.
  3. Create or select a workspace.
  4. Use workspace-scoped endpoints for backtests and paper trade plans.
  5. Keep real trading disabled unless an execution layer is intentionally added.

Current Behavior

Default local mode:

  • AUTH_MODE=none
  • one local user
  • one default local workspace
  • shared DuckDB market data from file.db
  • workspace-scoped durable app state in app_state.db
  • persisted workspaces, paper trade plans, paper fills, order placeholders, and backtest metadata/results

Password mode:

  • set AUTH_MODE=password
  • set DEFAULT_USER_PASSWORD before first startup to bootstrap the local owner
  • login uses an HTTP-only session cookie
  • workspace owners can add members and assign owner, editor, trader, or viewer roles

Market-data endpoints remain global because candle data is shared:

GET /api/v1/symbols
GET /api/v1/candles
GET /api/v1/ratios
GET /api/v1/scanner/uptrends
GET /api/v1/scanner/ratio-uptrends

User-created research and paper-trading objects can be accessed through workspace-scoped endpoints.

Start The App

Start the backend:

uv run uvicorn main:app --host 127.0.0.1 --port 8000

Start the frontend:

npm run dev

Open:

http://127.0.0.1:5173/

The frontend top bar shows the active workspace, lets you create another workspace, and lets you switch between loaded workspaces. Live paper trade plans and category backtests use the selected workspace.

App-State Database

The backend stores multiuser app state in:

app_state.db

Override the path with:

APP_STATE_DB_PATH=/path/to/app_state.db uv run uvicorn main:app --host 127.0.0.1 --port 8000

This SQLite database is separate from the market-data DuckDB file file.db. Deleting app_state.db removes saved workspaces, paper trade plans, fills, order placeholders, and persisted backtest metadata/results. It does not delete market candles.

Inspect The Current Session

Check the active local user and workspace:

curl http://127.0.0.1:8000/api/v1/auth/session

Expected response shape:

{
  "auth_mode": "none",
  "user": {
    "id": "local-user",
    "email": "local@example.invalid",
    "display_name": "Local User",
    "status": "active"
  },
  "active_workspace_id": "local-workspace",
  "workspaces": [
    {
      "id": "local-workspace",
      "name": "Local Workspace",
      "default_quote_asset": "USDT",
      "role": "owner"
    }
  ]
}

Equivalent user/session endpoint:

curl http://127.0.0.1:8000/api/v1/me

Enable Password Login

Start the backend with password auth:

AUTH_MODE=password DEFAULT_USER_PASSWORD='change-this-password' \
  uv run uvicorn main:app --host 127.0.0.1 --port 8000

Then open the frontend. The app shows a login screen before the chart workspace. The bootstrap user is:

local@example.invalid

Use the value of DEFAULT_USER_PASSWORD as the password. The session is stored in an HTTP-only cookie.

Login through the API:

curl -i -X POST http://127.0.0.1:8000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"local@example.invalid","password":"change-this-password"}'

Logout:

curl -X POST http://127.0.0.1:8000/api/v1/auth/logout

List And Create Workspaces

List workspaces:

curl http://127.0.0.1:8000/api/v1/workspaces

Create a workspace:

curl -X POST http://127.0.0.1:8000/api/v1/workspaces \
  -H 'Content-Type: application/json' \
  -d '{"name":"Research Team","default_quote_asset":"USDT"}'

The response includes the new id. Use that id as {workspace_id} in the workspace endpoints below.

Get one workspace:

curl http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}

Manage Workspace Members

Workspace owners can add or update members from the frontend top bar. The API endpoint is:

POST /api/v1/workspaces/{workspace_id}/members

Example:

curl -X POST http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/members \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "member@example.invalid",
    "display_name": "Member",
    "role": "trader",
    "password": "member-password"
  }'

List members:

GET /api/v1/workspaces/{workspace_id}/members

Legacy Local Routes

The original routes still work and map to the default local workspace:

POST /api/v1/backtests
POST /api/v1/backtests/category-relative-momentum
GET  /api/v1/backtests/{backtest_id}
GET  /api/v1/trade-plans
POST /api/v1/trade-plans
GET  /api/v1/accounts
GET  /api/v1/orders
POST /api/v1/orders

Objects created through a non-default workspace route are not visible through these legacy routes.

Workspace Backtests

Create a simple backtest in a workspace:

curl -X POST http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/backtests \
  -H 'Content-Type: application/json' \
  -d '{
    "strategy_id": "ma_cross",
    "symbol": "BTCUSDT",
    "interval": "1d",
    "from": 1704067200,
    "to": 1735689600,
    "initial_capital": 10000
  }'

Create a category relative momentum backtest:

curl -X POST http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/backtests/category-relative-momentum \
  -H 'Content-Type: application/json' \
  -d '{
    "category": "Layer 1",
    "strategy": "relative_momentum",
    "interval": "1d",
    "quote_asset": "USDT",
    "initial_capital": 10000,
    "long_count": 5,
    "short_count": 5
  }'

Read backtest results:

GET /api/v1/workspaces/{workspace_id}/backtests/{backtest_id}
GET /api/v1/workspaces/{workspace_id}/backtests/{backtest_id}/equity
GET /api/v1/workspaces/{workspace_id}/backtests/{backtest_id}/trades
GET /api/v1/workspaces/{workspace_id}/backtests/{backtest_id}/rebalances

Backtests are still computed synchronously in the API process. Durable queued jobs are planned for a later phase.

Workspace Paper Trade Plans

List trade plans:

curl http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/trade-plans

Create a single-leg paper trade plan:

curl -X POST http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/trade-plans \
  -H 'Content-Type: application/json' \
  -d '{
    "account_id": "paper-main",
    "trade_type": "single",
    "strategy_tag": "trend_following",
    "thesis": "Trend continuation",
    "invalidation": "Daily close below support",
    "max_risk_amount": 100,
    "legs": [
      {
        "symbol": "BTCUSDT",
        "side": "long",
        "planned_quantity": 0.01,
        "planned_entry_price": 60000
      }
    ]
  }'

Read, cancel, open, close, and inspect a trade plan:

GET    /api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}
DELETE /api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}
POST   /api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}/paper-open
POST   /api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}/paper-close
GET    /api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}/pnl
GET    /api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}/fills

Paper open example with explicit mark price:

curl -X POST http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/trade-plans/{trade_plan_id}/paper-open \
  -H 'Content-Type: application/json' \
  -d '{"mark_prices":{"BTCUSDT":60000},"fee_bps":5}'

Workspace Accounts And Orders

List paper accounts:

curl http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/accounts

Read paper positions:

curl http://127.0.0.1:8000/api/v1/workspaces/{workspace_id}/accounts/paper-main/positions

Order endpoints are present as placeholders. They remain disabled when TRADING_MODE=readonly, which is the default.

GET    /api/v1/workspaces/{workspace_id}/orders
POST   /api/v1/workspaces/{workspace_id}/orders
DELETE /api/v1/workspaces/{workspace_id}/orders/{order_id}

If trading mode is enabled later, POST /orders requires an Idempotency-Key header.

Troubleshooting

Workspace changes disappear

Check APP_STATE_DB_PATH. Workspace, paper plan, fill, order placeholder, and backtest state is stored in the SQLite app-state database, not in the DuckDB market data file.

Login does not show the expected user

Confirm AUTH_MODE and DEFAULT_USER_PASSWORD were set before startup. In default local mode, the app uses the local bootstrap user and default workspace.

Paper trade endpoints return workspace errors

Use a workspace id returned by GET /api/v1/workspaces, and make sure the current user has access to that workspace.

Important Limitations

  • Workspace, backtest, trade plan, fill, and order state is durable in local SQLite, but it is not yet PostgreSQL-backed or multi-process safe.
  • Password authentication is local and SQLite-backed. It is useful for self-hosted/local teams, but it is not an OIDC/SAML enterprise identity integration.
  • The frontend exposes workspace selection and creation, but not member management, invitations, or role editing.
  • Category mappings still come from configs/symbol-categories.yaml; workspace category sets are planned but not implemented.
  • Real exchange order execution is not implemented.

The private application repository tracks the deeper multiuser architecture work behind these limitations.