Home · Apps · rl-bank-mvp
Status: Draft MVP contract for rl-bank-api + first customer/staff clients.
This doc defines the minimum viable set of customer and staff journeys for the fake bank, and the corresponding GraphQL schema (MVP only) that rl-bank-api must implement. It is intentionally narrow: accounts, transactions, internal transfers, and basic staff approval flows only.
No cards, loans, or KYC flows are included here.
Assume authentication/session is already handled by an IdP + JWT. All customer flows below start from an authenticated customer context.
sub (stable customer identifier)name, email)me to fetch the current customer profile and basic data needed to personalize the UI.customerAccounts.CHECKING, SAVINGS)ACTIVE, FROZEN)accountTransactions(accountId: ID!).POSTED, PENDING)customerAccounts.fromAccount (must be owned by the same customer)toAccount (different account but owned by the same customer)initiateTransfer mutation.fromAccountTransfer record and corresponding Transaction records.PENDING or COMPLETED state depending on demo rules:
COMPLETED) unless we later add risk controls.Transfer payload and shows a confirmation state (e.g. success screen or banner).Assume staff users log in via a staff portal (web/mobile) using the same IdP but different audience/roles. All flows start from an authenticated staff context.
sub (stable staff user identifier)name, email)staff, support_agent)staffMe to fetch the staff profile and any relevant capabilities/permissions.customerId (exact)searchCustomers — see schema).ACTIVE, SUSPENDED)customerAccounts(customerId: ID) with an explicit customerId.accountTransactions(accountId: ID!) (same query as customer but authorized for staff context) to show more history.pendingTransfersForApproval.PENDING state and require staff action.PENDING)approveTransfer)rejectTransfer)APPROVED and, if not yet settled, posts the necessary ledger movements.REJECTED.This section defines the MVP contract between rl-bank-api and clients. It is deliberately small; future slices can extend it via additional fields and mutations without breaking these basics.
Note: Names and shapes are written in GraphQL SDL. Implementation details (NestJS modules, resolvers, etc.) are out of scope here.
"Supported account types in the MVP."
enum AccountType {
CHECKING
SAVINGS
}
"High-level transaction categorisation."
enum TransactionType {
DEBIT
CREDIT
}
"Transaction posting state."
enum TransactionStatus {
PENDING
POSTED
REVERSED
}
"Lifecycle of an internal transfer."
enum TransferStatus {
PENDING
APPROVED
REJECTED
COMPLETED
FAILED
}
"Authenticated customer user."
type Customer {
id: ID!
name: String
email: String
}
"Authenticated staff user."
type StaffUser {
id: ID!
name: String
email: String
role: String
}
"Bank account owned by a customer."
type Account {
id: ID!
customerId: ID!
name: String
accountNumberMasked: String!
type: AccountType!
currency: String!
status: String! # e.g. ACTIVE, FROZEN, CLOSED (string for now to avoid over-specifying)
availableBalance: Float!
currentBalance: Float!
}
"Ledger transaction for an account."
type Transaction {
id: ID!
accountId: ID!
postedAt: String!
description: String!
amount: Float! # positive for credit, negative for debit in MVP
type: TransactionType!
status: TransactionStatus!
runningBalance: Float
}
"Internal transfer between customer-owned accounts."
type Transfer {
id: ID!
fromAccountId: ID!
toAccountId: ID!
amount: Float!
currency: String!
status: TransferStatus!
createdAt: String!
approvedAt: String
rejectedAt: String
completedAt: String
failureReason: String
description: String
}
"Returns the currently authenticated customer (self)."
me: Customer
"Returns accounts for the current customer.
When called in a customer context, `customerId` is derived from the token and
this field should be ignored. When called in a staff context, `customerId`
may be provided explicitly to view that customer's accounts."
customerAccounts(customerId: ID): [Account!]!
"Returns recent transactions for a specific account."
accountTransactions(accountId: ID!): [Transaction!]!
"Returns the currently authenticated staff user (self)."
staffMe: StaffUser
"Staff search for customers by name/email/id."
searchCustomers(
query: String!
page: Int = 0
pageSize: Int = 20
): [Customer!]!
"Transfers that are currently pending and require staff action."
pendingTransfersForApproval: [Transfer!]!
input InitiateTransferInput {
fromAccountId: ID!
toAccountId: ID!
amount: Float!
description: String
}
"Initiate an internal transfer between accounts owned by the same customer."
initiateTransfer(input: InitiateTransferInput!): Transfer!
"Approve a pending transfer as staff."
approveTransfer(transferId: ID!): Transfer!
"Reject a pending transfer as staff."
rejectTransfer(transferId: ID!, reason: String): Transfer!
initiateTransfer must verify both fromAccountId and toAccountId are owned by the authenticated customer.pendingTransfersForApproval, approveTransfer, rejectTransfer) require a staff role/permission; details of RBAC are out of scope for this doc.fromAccount to go below an implementation-defined minimum (e.g. zero) unless/until overdrafts are explicitly added to scope.