Skip to content

Family & Team Sub-Accounts with Scoped Permissions #286

Description

@robertocarlous

Problem Statement

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

Current State

Auth is built around a single requireAuth middleware (src/middleware/authenticate.ts) that resolves a JWT down to a Session row (the JWT itself only carries { id: userId }; the session row, keyed by the raw token, carries userId/walletAddress/network) and sets req.userId/req.auth. enforceUserAccess then checks that the authenticated user matches the :userId route param. There is currently no concept of "acting on behalf of another user" anywhere in the middleware chain — this issue introduces one.

Proposed Solution

Data Model

model SubAccount {
  id             String            @id @default(uuid())
  parentUserId   String
  childUserId    String
  permissions    SubAccountPermission[]
  status         SubAccountStatus  @default(ACTIVE)
  createdAt      DateTime          @default(now())
  revokedAt      DateTime?
  parent         User @relation("ParentOf", fields: [parentUserId], references: [id], onDelete: Cascade)
  child          User @relation("ChildOf", fields: [childUserId], references: [id], onDelete: Cascade)
  @@unique([parentUserId, childUserId])
}
enum SubAccountPermission { VIEW DEPOSIT WITHDRAW MANAGE_STRATEGY }
enum SubAccountStatus { ACTIVE REVOKED }

API Surface

  • POST /api/admin/sub-accounts (or under /api/portfolio, TBD during implementation) — parent creates/invites a sub-account relationship with an initial permission set. requireAuth.
  • PATCH /api/sub-accounts/:id/permissions — parent adjusts permissions; ownership check against subAccount.parentUserId.
  • DELETE /api/sub-accounts/:id — revoke, setting status = REVOKED and revokedAt, not a hard delete (preserve the audit trail of what was once granted).

Integration Points

  • Extend src/middleware/authenticate.ts (or add a new middleware layered after it) to resolve an "acting as" context: when a request includes a target user id that differs from req.auth.userId, look up an ACTIVE SubAccount row linking them and check the required permission for that route before proceeding — otherwise reject. This should be a new, explicit middleware (e.g. requireSubAccountPermission(permission)) rather than quietly overloading enforceUserAccess, so the existing self-access path is untouched and this is purely additive.
  • Every delegated action (a parent depositing/withdrawing/changing strategy on a child's behalf) must be attributed distinctly wherever actions are already logged — Transaction and AgentLog — so audit trails can distinguish "the child did this themselves" from "the parent did this on the child's behalf." This likely means adding an actingAsUserId-style field alongside the existing userId on those records, not overloading userId itself.
  • Revocation must take effect immediately: since Session/JWT resolution already happens on every request (there's no long-lived cached permission set), revoking a SubAccount permission should be effective on the very next request with no separate cache-invalidation step needed — but this must be verified with a test, not assumed.

Edge Cases & Failure Modes

  • Parent revokes WITHDRAW permission while a withdrawal initiated under the old permission is already in flight on-chain — the in-flight one should complete or fail on its own merits; only future actions are blocked.
  • Circular or chained sub-account relationships (a sub-account itself granting sub-accounts) — explicitly disallowed for v1; enforce at creation time.
  • A permission is requested that was never granted (e.g. MANAGE_STRATEGY without DEPOSIT) — permissions are independent flags, not a hierarchy; test that having one doesn't implicitly grant another.
  • Child account also has its own independent login and can act on itself normally — sub-account permissions only affect the parent's ability to act on the child, never the child's own full self-access.

Security & Privacy Considerations

  • This is the most security-sensitive item in this batch since it's explicitly building a "user A can spend user B's money" pathway — every route gaining delegated access must be enumerated explicitly (deposit, withdraw, strategy-change) rather than delegated access being a blanket bypass of all auth checks.
  • Denial-path tests matter as much as happy-path tests here: a request for a permission that was never granted, or was revoked, must fail closed (403), and this needs to be tested at least as thoroughly as the success path.
  • AdminAuditLog (already used for admin-scope actions per src/middleware/adminAuth.ts) is a reasonable precedent to follow for how delegated actions get logged, though this is a user-to-user delegation, not an admin action — a parallel, user-facing audit trail should be visible to both parent and child, not buried in an admin-only log.

Out of Scope

  • Multi-parent sub-accounts (one child, multiple parents) — single parent per sub-account for v1.
  • Sub-account-initiated sub-accounts (no nesting, per Edge Cases above).

Suggested Implementation Plan

  1. SubAccount model, migration, and parent-facing CRUD with explicit permission grants.
  2. requireSubAccountPermission(permission) middleware, layered onto deposit/withdraw/strategy routes without touching the existing self-access path.
  3. Delegated-action attribution on Transaction/AgentLog.
  4. Revocation-takes-effect-immediately test coverage, plus the full denial-path test suite.

Acceptance Criteria

  • SubAccount model + migration; parent-managed CRUD with explicit per-action permission grants
  • New middleware enforces permission scope on deposit/withdraw/strategy-change routes without altering existing self-access behavior for non-delegated requests
  • Delegated actions are distinguishably attributed in Transaction/AgentLog, visible to both parent and child
  • Revoking a permission takes effect on the very next request — verified by a test, not just assumed
  • Tests covering every permission-denial path (missing permission, revoked permission, non-existent relationship) in addition to the happy path
  • docs/openapi.yaml updated

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions