Skip to content

feat: Family & Team Sub-Accounts with Scoped Permissions - #306

Merged
robertocarlous merged 1 commit into
Neurowealth:mainfrom
dotunv:feat/sub-accounts-scoped-permissions
Jul 28, 2026
Merged

feat: Family & Team Sub-Accounts with Scoped Permissions#306
robertocarlous merged 1 commit into
Neurowealth:mainfrom
dotunv:feat/sub-accounts-scoped-permissions

Conversation

@dotunv

@dotunv dotunv commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Closes #286

Problem

The platform is single-user-per-wallet. A parent wanting to manage a child's savings, or a small team wanting a shared treasury, has no way to do that without sharing one account's full credentials — meaning full withdraw/strategy-change power, with no audit trail distinguishing who actually took an action.

Solution

Adds sub-accounts with scoped, revocable permissions.

Data Model

  • SubAccount model with parentUserId, childUserId, permissions[], status (ACTIVE/REVOKED)
  • SubAccountPermission enum: VIEW, DEPOSIT, WITHDRAW, MANAGE_STRATEGY (independent flags, not hierarchical)
  • actingAsUserId field on Transaction and AgentLog for delegated-action audit trail

New Middleware

  • requireSubAccountPermission(permission) — layered after requireAuth, checks for active sub-account relationship + required permission. Self-access passes through untouched.

API Surface

Method Endpoint Description
POST /api/v1/sub-accounts Create sub-account relationship with initial permissions
PATCH /api/v1/sub-accounts/:id/permissions Update granted permissions
DELETE /api/v1/sub-accounts/:id Soft-revoke (preserves audit trail)

Integration Points

  • Deposit and withdraw routes now use requireSubAccountPermission('DEPOSIT') / requireSubAccountPermission('WITHDRAW')
  • Transaction controller updated to allow delegated access and persist actingAsUserId

Security Guarantees

  • Self-access untouched: When req.auth.userId === targetUserId, middleware is a no-op
  • Revocation immediate: No caching — every request hits the DB; verified by test
  • No chaining: Child cannot be a parent of their own sub-accounts (enforced at creation)
  • Flat permissions: Having DEPOSIT does not implicitly grant VIEW
  • Denial paths: 403 for missing permission, revoked permission, non-existent relationship
  • Audit trail: Transaction.actingAsUserId and AgentLog.actingAsUserId distinguish delegated from self-actions, visible to both parent and child

Tests (27 new, 22 existing unaffected)

  • 14 middleware tests: self-access, delegation happy path, all denial paths, revocation-immediately, independent permissions, error handling
  • 13 CRUD route tests: full CRUD, ownership checks, chained/rejected scenarios, revocation-immediately integration

Files Changed

  • prisma/schema.prisma — SubAccount model, enums, actingAsUserId fields
  • prisma/migrations/20260728000000_add_sub_accounts/ — migration SQL
  • src/middleware/subAccount.ts — new permission middleware
  • src/routes/sub-accounts.ts — CRUD endpoints
  • src/routes/deposit.ts, src/routes/withdraw.ts — middleware integration
  • src/controllers/transaction-controller.ts — delegation support + actingAsUserId
  • src/types/express.d.ts — type augmentation
  • src/index.ts — route mounting
  • docs/openapi.yaml — schema + endpoint documentation
  • tests/unit/middleware/subAccount.test.ts, tests/unit/routes/sub-accounts.test.ts — test suite

Add SubAccount model with granular permissions (VIEW, DEPOSIT, WITHDRAW,
MANAGE_STRATEGY) allowing a parent user to delegate limited access to a
child user's account. Permissions are independent flags, not hierarchical.

New middleware requireSubAccountPermission(permission) enforces delegation
on deposit/withdraw routes without altering the existing self-access path.
Delegated actions are attributed via actingAsUserId on Transaction and
AgentLog for a complete audit trail visible to both parent and child.

CRUD endpoints:
- POST /api/v1/sub-accounts — create relationship with initial permissions
- PATCH /api/v1/sub-accounts/:id/permissions — adjust granted permissions
- DELETE /api/v1/sub-accounts/:id — soft-revoke (preserves audit trail)

Security guarantees:
- Self-access passes through untouched (no DB lookup)
- Revocation takes effect on the very next request (no caching)
- No chained sub-accounts (child cannot be a parent)
- Denial paths return 403: missing permission, revoked, non-existent

Closes Neurowealth#286
@robertocarlous
robertocarlous merged commit a9fef8e into Neurowealth:main Jul 28, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Family & Team Sub-Accounts with Scoped Permissions

3 participants