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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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