Skip to content

Crest — API Endpoints ​

ProductCrest — Personal Finance App
Document version0.1 (Draft)
Date2026-10-06
Based onTechnicalDesign.md §9.1, ApiModuleStandard.md, BusinessRequirements.md, UserJourneys.md
PrototypesPrototype/CrestPublicSite.dc.html, Prototype/CrestAdminPortal.dc.html, Prototype/CrestPrototypeV3.dc.html
OwnerReizkian Y. Radityatama
StatusPlan. The OpenAPI file stays the contract once an endpoint is built; this document says what to build and why

For every screen in the three prototypes this document says which endpoint it calls, which requirement (FR/BR) and journey (UJ) it serves, and what goes in and out. The Technical Design §9.1 lists the modules; this document goes down to the request and response of each endpoint.

Status column: Built = in Packages/ApiContract/OpenApi.json today. M3, M4, M5 = the milestone in TechnicalDesign.md §18. P2 = Phase 2.


1. Rules that apply to every endpoint ​

Details are in ApiModuleStandard.md; only what a reader of the tables below needs is repeated here.

  • All paths are under /v1, lowercase kebab-case plural nouns, JSON fields camelCase.
  • Auth column: Public = @Public() (listed in Test/Integration/RouteAccessSpec.ts); User = any logged-in user; Admin = Super Admin in a two-factor-verified admin session (SuperAdminGuard); Owner / Member = role in the space in the path.
  • Money is an integer in minor units ("amount": 45000 is IDR 45,000; USD 3.20 is 320). Never a decimal, never a string. A transaction's amount is signed (BRD §6.3). Exchange rates are strings of at most 12 decimals, because they are not money.
  • Bodies, queries, and responses are always named (…Body, …Query, …Response). A route with nothing to return declares 204 or 202, and has no body (ApiModuleStandard.md §1.4).
  • Ids are UUIDs. A create may send its own id; sending it again changes nothing (BR-48).
  • Dates: createdAt of a transaction is its date and time (BR rule 5). All timestamps are ISO 8601 UTC.
  • Lists return { items, nextCursor } with limit (1–100, default 20) and cursor. Filters are flat; arrays are repeated keys.
  • Related records come back as summaries: { "id": "…", "name": "…" }.
  • Errors have one shape (type, title, status, detail, code, requestId, optional errors[]). The tables list the code values a client must handle; every endpoint can also return Unauthenticated (401, unless Public), RateLimited (429), and InternalError (500), which are not repeated.

2. Public site (Apps/Site) ​

Prototype: CrestPublicSite.dc.html. The home, install, privacy, and terms pages are static and call nothing. Three pages call the API.

PageEndpointAuthRequirementsJourneysStatus
Request accessPOST /access-requestsPublicFR-REQ-1 to 4, FR-REQ-6, BR-14, BR-44UJ-01, UJ-02Built
Activate your loginPOST /auth/activatePublicFR-AUTH-2, FR-AUTH-3, BR-10, FR-APP-7UJ-03Built
Reset your password (ask)POST /auth/password/forgotPublicFR-AUTH-7, FR-APP-7UJ-05Built
Reset your password (set)POST /auth/password/resetPublicFR-AUTH-7, FR-APP-7UJ-05Built

POST /access-requests — Built ​

Also used by the app's Request access screen (§5.1), so the site and the app send the same body.

Request:

json
{
  "id": "0190f0e2-6c6e-7c2a-9f3b-2a1f4e6d9a10",
  "fullName": "Dewi Septaria",
  "email": "dewi@example.com",
  "country": "ID",
  "message": "Rina recommended Crest. I want to track our household spending.",
  "acceptedTerms": true,
  "confirmedAdult": true,
  "formOpenedAt": "2026-10-06T08:01:12Z"
}

website is the hidden honeypot and must be left out or empty; formOpenedAt is when the form was shown (§9.7: a submission within seconds is treated as a bot).

Response 202 Accepted, the same for a new, a duplicate, and a recently rejected email (FR-REQ-4, BR-14); no body that says which it was. Errors: ValidationFailed, RateLimited (3 per hour and 10 per day per IP hash, §9.7).

POST /auth/activate — Built ​

json
{ "token": "…", "password": "at least 10 characters" }

Response 204. Errors: NotFound (404: the link is unknown, used, or expired after 72 hours, all the same answer so a probe learns nothing; the site's set-password.js already shows its "link expired" text for it), ValidationFailed (a password under 10 characters). The common-password list of Technical Design §8.1 is not checked yet. The page's "The two passwords don't match" check is client-side only, so the confirm field is not sent.

POST /auth/password/forgot — Built ​

json
{ "email": "dewi@example.com" }

Response 202 with no body, always, whether or not the email has a login. Sends a 1-hour link to /reset-password?token=… only to an Active user, over SMTP: Brevo in production, Mailpit while developing (read the message at http://localhost:8025). The site has no page with an email field for this call yet; the reset page only takes the token.

POST /auth/password/reset — Built ​

json
{ "token": "…", "password": "…" }

Response 204. Revokes every session of the user. Errors: as activate. A deactivated user's link does not work.


3. Super Admin portal (Apps/Admin) ​

Prototype: CrestAdminPortal.dc.html (Login, Overview, Access requests, Users, Audit log, Settings). Every endpoint is under /v1/admin/… and has SuperAdminGuard, except login, which is how the session starts.

3.1 Login and two-factor ​

Requirements: FR-ADM-1, FR-ADM-2, FR-ADM-2a, NFR-11

Journeys: UJ-09, UJ-15

The prototype's login has four stages: password, six-digit code (or backup code), first-time setup with a QR code, and backup codes.

StageEndpointAuthStatus
PasswordPOST /auth/login with client: "admin"PublicM3
Code or backup codePOST /auth/totp/verifyAdmin session, not yet verifiedM3
First time: QR and keyPOST /auth/totp/setupAdmin session, not yet verifiedM3
First time: confirm the first code, receive backup codesPOST /auth/totp/confirmsameM3
Log out, 30 minutes idlePOST /auth/logoutUserM3
Keep the session alivePOST /auth/refresh (cookie __Host-crest_admin)Public (cookie)M3
json
// POST /auth/login
{ "email": "reizkian@crest.app", "password": "…", "client": "Admin" }
// 200: the access token is in the body, the refresh token is in the cookie
{ "accessToken": "…", "expiresIn": 900, "twoFactor": "Required" }

twoFactor is "Required" (enter a code), "SetupRequired" (first login of a new Super Admin, FR-ADM-16), or "NotRequired" (app client). Errors: InvalidCredentials (401, the same for a wrong email, a wrong password, an unactivated user, and a deactivated user), RateLimited (growing delay per email).

json
// POST /auth/totp/verify
{ "code": "123456" }            // or { "backupCode": "XXXX-XXXX" }
// 200
{ "accessToken": "…", "expiresIn": 900 }
// POST /auth/totp/setup  → 200
{ "otpauthUri": "otpauth://totp/Crest:…?secret=…", "secret": "JBSWY3DPEHPK3PXP" }
// POST /auth/totp/confirm  { "code": "123456" } → 200
{ "backupCodes": ["ABCD-1234", "…"] }   // shown once; "Download .txt" is made by the browser

Errors: InvalidTotpCode, InvalidBackupCode. A used backup code stops working (FR-ADM-2a).

3.2 Overview ​

Requirements: FR-ADM-15

Journeys: UJ-09, UJ-15

GET /admin/overview · Admin · M3

json
{
  "pendingRequests": { "count": 3, "today": 1, "oldestDays": 4 },
  "activeUsers": { "count": 4, "signedInToday": 2 },
  "invitedUsers": { "count": 2, "bounced": 1 },
  "newUsers": { "thisMonth": 3, "thisWeek": 1 },
  "pendingTop": [ { "id": "…", "fullName": "Dina Pratama", "email": "dina@example.com", "createdAt": "…" } ],
  "recentActivity": [ { "id": "…", "action": "RequestApprove", "actor": { "id": "…", "name": "…" }, "target": "…", "createdAt": "…" } ]
}

The four cards and the two lists are one call, so the page opens with one request. pendingTop is the 3 newest pending requests; recentActivity is the 5 newest audit entries (the same shape as §3.5).

3.3 Access requests ​

Requirements: FR-ADM-4 to FR-ADM-6a, FR-ADM-13, FR-ADM-18

Journeys: UJ-09, UJ-10

Screen elementEndpointRequirementsStatus
Tabs Pending / Approved / Rejected with counts, list, newest firstGET /admin/access-requests?status=Pending&limit=&cursor=FR-ADM-4Built (the counts are not in the response yet, see §7)
Detail panelthe list item, no separate callFR-ADM-4—
Approve (also re-approve a rejected one)POST /admin/access-requests/{id}/approveFR-ADM-5, FR-ADM-6aM3
Reject, with an internal notePOST /admin/access-requests/{id}/rejectFR-ADM-6M3
Select several, Approve selected / Reject selectedPOST /admin/access-requests/approve and /reject with idsFR-ADM-18M3

List item (today's AccessRequestResponse, plus the three fields the detail panel needs):

json
{
  "id": "…", "fullName": "Dina Pratama", "email": "dina@example.com", "country": "ID",
  "message": "…", "status": "Approved", "rejectionNote": null, "createdAt": "…",
  "decidedAt": "2026-09-21T09:30:00Z",
  "decidedBy": { "id": "…", "name": "Reizkian Yesaya" },
  "user": { "id": "…", "name": "Dina Pratama", "status": "Invited" }
}

user is null until approval; the panel shows "User is invited. Open user" from it. Approve returns the updated request, so the screen needs no second call. Reject body: { "note": "No referral and a vague reason." } (optional; never emailed). Bulk bodies: { "ids": ["…"], "note": "…" } and the response is { "succeeded": ["…"], "failed": [{ "id": "…", "code": "Conflict" }] }, because one stale row must not undo the others.

Errors: NotFound, Conflict (already approved; reject on a non-pending request). Approve sends the activation email, and a Reject sends the polite no-reason email (FR-REQ-5). Both are written to the audit log in the same transaction.

3.4 Users ​

Requirements: FR-ADM-7 to FR-ADM-12, FR-ADM-16, FR-ADM-17

Journeys: UJ-11 to UJ-14

Screen elementEndpointRequirementsStatus
List with search, status tabs, sortable columnsGET /admin/users?search=&status=&sort=&direction=&limit=&cursor=FR-ADM-8M3
Detail panelthe list itemFR-ADM-8—
+ New userPOST /admin/usersFR-ADM-7, FR-ADM-7aM3
Edit name or emailPATCH /admin/users/{id}FR-ADM-12M3
Resend activation linkPOST /admin/users/{id}/activation-linkFR-AUTH-3, FR-ADM-10M3
Send password-reset emailPOST /admin/users/{id}/password-resetFR-ADM-10M3
Deactivate / ReactivatePOST /admin/users/{id}/deactivate, /reactivateFR-ADM-9, FR-AUTH-9, BR-11, BR-13M3
Make / remove Super AdminPUT /admin/users/{id}/super-adminFR-ADM-16, BR-13M3
Delete permanently (type the email)DELETE /admin/users/{id}FR-ADM-11, FR-AUTH-8, BR-11M3

List item. The panel's rows (Created, Last login, Activation link, Email change) come straight from these fields; there is nothing about spaces or money (FR-ADM-17):

json
{
  "id": "…", "fullName": "Maria Santos", "email": "maria@example.ph",
  "status": "Invited", "isSuperAdmin": false,
  "createdAt": "…", "lastLoginAt": null,
  "activationLink": { "sentAt": "…", "expiresAt": "…", "state": "Sent" },
  "pendingEmail": null
}

activationLink.state is Sent, Expired, or Bounced ("Invitation email bounced" in the list); it is null for an Active user. status is Invited, Active, or Deactivated. Sort keys: name, createdAt, lastLoginAt.

Bodies:

json
// POST /admin/users            → 201 the user, or 200 the approved request when the email had a pending one (FR-ADM-7a)
{ "fullName": "Lina Chen", "email": "lina@example.tw" }
// PATCH /admin/users/{id}      (an omitted field is unchanged)
{ "fullName": "Lina C.", "email": "lina@example.com" }
// PUT /admin/users/{id}/super-admin
{ "isSuperAdmin": true }
// DELETE /admin/users/{id}
{ "confirmEmail": "budi@example.com" }

DELETE takes a body because the typed confirmation is checked on the server too (the portal's check is only a convenience). The PATCH email rules follow §8.6 of the Technical Design: an Invited user's email changes at once and a new link is sent; an Active user's change waits for the new address to confirm, so the response has pendingEmail set and email unchanged.

Errors: EmailTaken (BR-9, the portal shows "Already a user: Maria · Invited" with an Open link, so this error carries existingUserId), NotFound, LastSuperAdmin (BR-13), CannotChangeSelf (the prototype's "This is you" note: no self deactivate, delete, or role change), ConfirmationMismatch, Conflict (wrong status for the action).

One more endpoint is public: POST /auth/email-change/confirm with { "token": "…" } (the link sent to the new address) · M3 · FR-ADM-12 · UJ-14. It needs a small page on the public site that the prototype does not draw yet.

3.5 Audit log ​

Requirements: FR-ADM-14, NFR-12

Journeys: UJ-09 to UJ-15

GET /admin/audit-log?type=&range=&limit=&cursor= · Admin · M3

QueryValuesScreen
typeRequests, Users, Roles, Deletions, SignInsthe type chips (All = omit)
rangeToday, Last7Days, Last30Days (All = omit)the range control
json
{
  "items": [
    {
      "id": "…", "createdAt": "2026-09-23T16:40:00Z",
      "actor": { "id": "…", "name": "Reizkian Yesaya" },
      "type": "Users", "action": "UserPasswordResetSent",
      "target": { "name": "Ayu Lestari", "email": "ayu@example.com" }
    }
  ],
  "nextCursor": null
}

actor is null for the setup script ("Setup script" in the prototype). action is a stable code (RequestApprove, UserDeactivate, UserSuperAdminGrant, AdminTwoFactorReset); the portal turns it into the sentence it shows. target carries the name and email as they were when the action happened, because the user may be deleted later (the table keeps no foreign key for this reason). Read only: there is no update or delete (NFR-12).

3.6 Settings (appearance) ​

The portal's Settings page says its mode and color are "the same as your Crest app. Changing them here changes them there too." That is the signed-in user's theme, so it uses GET and PATCH /me (§5.2); there is no admin endpoint. The color theme (the accent) is not in the data model today, see §7.


4. Auth shared by the portal and the app ​

EndpointBodyResponseUsed byRequirementsJourneysStatus
POST /auth/loginemail, password, client (Mobile or Admin)accessToken, expiresIn, twoFactor; refresh cookieApp, portalFR-AUTH-4, FR-AUTH-5, BR-20UJ-04, UJ-52M3
POST /auth/refreshnone (cookie); native apps send refreshTokennew accessToken, rotated cookieApp, portalFR-AUTH-9UJ-04, UJ-07M3
POST /auth/logoutnone204, revokes this sessionApp, portal—UJ-52M3
PUT /auth/pinpin (4–6 digits)204AppFR-AUTH-6UJ-04, UJ-16, UJ-37M3
DELETE /auth/pinnone204AppFR-AUTH-6UJ-37M3
POST /auth/pin/verifypin204, or InvalidPin and the attempts leftAppFR-AUTH-6, §8.4UJ-04M3

/auth/pin/verify is needed because the prototype says a wrong PIN on a connected device counts toward 5 attempts and then logs out; the device also checks its own slow hash offline, so this call happens only when online. A forced logout (deactivated, deleted, revoked session) shows up as 401 Unauthenticated on any call, and on sync/pull in particular (BR-50).


5. The Crest app (Apps/Mobile, prototype CrestPrototypeV3.dc.html) ​

The app is local-first. In local mode it calls no endpoint at all (FR-MOD-2, BR-46): the Welcome, Onboarding, Home, Activity, Stats, Budgets, Financial accounts, Categories, Recurring, Goals, Exchange rates, Backup, Export, and Erase screens read and write the database on the device. Their rules live in the app and in Packages/MoneyTestVectors. The API enters in connected mode, in the three groups below.

5.1 Before connecting: the Connect, Request access, and Forgot password screens ​

ScreenEndpointRequirementsJourneysStatus
Connect to Crest › Log inPOST /auth/login (client: "mobile")FR-MOD-9, FR-AUTH-4UJ-52, UJ-04M3
Request access (inside the app)POST /access-requests (§2)FR-REQ-1aUJ-01Built
Forgot password?opens the public site's reset pageFR-AUTH-7UJ-05—
Version check at startGET /app-config (and the static app-config.json in local mode)FR-APP-5UJ-39Built

The prototype's Unlock and Forgot PIN screens are device-only in local mode; in connected mode POST /auth/pin/verify (§4) applies.

5.2 Profile and settings (More › settings) ​

EndpointBody or responseRequirementsJourneysStatus
GET /meid, fullName, email, baseCurrency, language, timeZone, theme, dailyReminderTime, isSuperAdmin, hasPin, statusFR-SET-1 to 3UJ-37M3
PATCH /meany of fullName, baseCurrency, language, timeZone, theme, dailyReminderTimeFR-SET-1 to 3, FR-NOT-1UJ-16, UJ-37M3
DELETE /me{ "confirm": "DELETE" } (the prototype asks the person to type DELETE)FR-AUTH-8, BR-11, BR-34UJ-08M3
GET /me/exchange-rates{ items: [{ currency, rateToBase, updatedAt }] } (not paged: one row per currency)FR-FIN-6, FR-FIN-7, BR-15UJ-35M4
PUT /me/exchange-rates/{currency}{ "rateToBase": "15850.5" }FR-FIN-6, FR-TRX-2dUJ-22, UJ-35M4

PATCH /me with a new baseCurrency also changes the Personal space's currency (a database trigger does this). The client shows the FR-SET-4 warning first; the API does not block it (BR-23).

5.3 Sync and first upload ​

Requirements: FR-MOD-9 to FR-MOD-12, BR-21, BR-47, BR-48

Journeys: UJ-52, UJ-25, UJ-07

Step on screenEndpointStatus
After login: "Upload this device's data" or "Start fresh"GET /me/sync-stateM4
UploadPOST /sync/import (chunks)M4
Every day usePOST /sync/push, then GET /sync/pullM4
The "Waiting to sync" label on a rowlocal outbox, no endpoint—
json
// GET /me/sync-state
{ "personalSpaceId": "…", "personalSpaceEmpty": true }

// POST /sync/import   (one chunk; each chunk is one transaction)
{
  "spaceId": "…",
  "settings": { "baseCurrency": "IDR", "language": "id", "timeZone": "Asia/Jakarta", "budgetStartDay": 25 },
  "rows": {
    "categories": [], "financialAccounts": [], "transactions": [], "budgets": [], "savingsGoals": []
  }
}
// 200
{ "accepted": 120, "rejected": [ { "table": "Transactions", "id": "…", "code": "CurrencyMismatch" } ] }
// 409 Conflict when the Personal space is no longer empty
json
// POST /sync/push
{ "changes": [
    { "table": "Transactions", "op": "Upsert", "id": "…", "baseUpdatedAt": null,
      "row": { "spaceId": "…", "financialAccountId": "…", "kind": "Expense", "amount": -45000,
               "categoryId": "…", "note": "Lunch", "createdAt": "2026-09-29T05:12:00Z" } },
    { "table": "Transactions", "op": "Delete", "id": "…" } ] }
// 200, one result per change, in order
{ "results": [ { "id": "…", "status": "Applied" }, { "id": "…", "status": "Rejected", "code": "CategoryWrongSpace" } ] }

// GET /sync/pull?cursor=…&limit=500
{ "spaces": [ { "spaceId": "…", "changes": [ { "table": "Transactions", "id": "…", "deleted": false, "row": { } } ] } ],
  "nextCursor": "…", "hasMore": false }

table is one of Spaces, SpaceMembers, FinancialAccounts, Categories, Transactions, Transfers, Budgets, SavingsGoals, GoalContributions, Recurrences. In row, related records use ids (not summaries): the device keeps its own copy and builds the screen from it, so the "summaries, never ids" rule in the API standard is for REST lists, not for sync rows. Rejection codes: AmountSignInvalid, CategoryWrongSpace (BR-31), CurrencyMismatch and CurrencyLocked (BR-1, BR-26), ForbiddenRow (BR-36), NotAMember.

5.4 Spaces, members, and invitations ​

Milestone: M5

Requirements: FR-SPC-1 to FR-SPC-17

Journeys: UJ-42 to UJ-49

These cover the prototype's Switch space list, + New shared space, the invitation banner on Home ("Ayu invited you to Bali trip"), Members, and Space settings. Data inside a space (accounts, transactions, budgets) arrives by sync; these REST endpoints are for the actions that need an immediate server answer.

Screen elementEndpointAuthRequirementsJourneys
Space switcher, with balances computed on the deviceGET /spacesUserFR-SPC-11UJ-30, UJ-46
Create space (name, currency, budget start day, invite emails)POST /spacesUserFR-SPC-2, FR-SPC-3, FR-SPC-12UJ-42
Rename, currency, budget start day, time zonePATCH /spaces/{id}OwnerFR-SPC-12, FR-SET-3, FR-SET-4UJ-42
Delete space (type the name)DELETE /spaces/{id}OwnerFR-SPC-10UJ-49
Members listGET /spaces/{id}/membersMemberFR-SPC-6UJ-43
Change rolePATCH /spaces/{id}/members/{userId} { "role": "Owner" }OwnerFR-SPC-6UJ-49
My "Include in my totals" and "Notify me"PATCH /spaces/{id}/members/me { "includeInTotals": true, "notifyNewTransaction": false }MemberFR-SPC-13, FR-SPC-16UJ-31, UJ-44
Remove a member or leave (userId or me)DELETE /spaces/{id}/members/{userId}Owner, or the member themselvesFR-SPC-8, FR-SPC-9UJ-48
Invite by emailPOST /spaces/{id}/invitations { "email": "…", "role": "Member" }OwnerFR-SPC-4UJ-43
Pending invitations, cancelGET /spaces/{id}/invitations, DELETE /invitations/{id}OwnerFR-SPC-5UJ-43
My invitations, the banner on HomeGET /me/invitationsUserFR-SPC-4, FR-NOT-4UJ-43
Join / Not nowPOST /invitations/{id}/accept, /declineUser (the invitee)FR-SPC-4, BR-39UJ-43

Invite response is always 202 with no sign of whether the email has a login (FR-SPC-4). GET /me/invitations items carry space: { id, name } and invitedBy: { id, name }, which is all the banner shows. Cross-space transfers (FR-SPC-14) are POST /transfers, PATCH /transfers/{id}, DELETE /transfers/{id} and write both legs in one database function, so they cannot go through sync one leg at a time:

json
// POST /transfers
{ "id": "…", "from": { "financialAccountId": "…", "amount": 500000 },
  "to": { "financialAccountId": "…" },
  "rate": null, "toAmount": null,
  "fee": { "amount": 2500 }, "note": "Top up the kitchen", "createdAt": "…" }

For a cross-currency transfer send rate or toAmount (either; the server computes the other, FR-TRX-2b). Errors: NotAMember (404 for a space the caller cannot see), LastOwner (FR-SPC-9), InvitationExpired, AlreadyMember.

5.5 REST lists the app may also call ​

Technical Design §9.1 lists REST CRUD for financial accounts, transactions, categories, budgets, and goals next to sync. With the app local-first, sync carries all of them and the REST endpoints are not needed for any screen in the prototype. Build them only when a client other than the app needs them. This keeps a long list of endpoints out of M4 and M5. The one list that stays REST is the shared-space "Added by" feed, if a later phase wants it without a local copy.

5.6 Notifications and the rest ​

ScreenEndpointStatus
Notification list (budget alerts, invitations, removed from a space)GET /notifications, POST /notifications/{id}/readP2 (FR-NOT-3)
Recurring entries made on the due dateworker job, no endpoint (FR-TRX-5b)P2
Export CSV, Backup file, Erase this deviceon the device, no endpoint—

6. Endpoint index ​

CountGroupStatus
7Health, app-config, POST /access-requests, GET /admin/access-requests, activate, forgot, resetBuilt
3Auth: login, refresh, logoutM3
6Auth: PIN (3), TOTP (3)M3
1Auth: email-change confirmM3
1GET /admin/overviewM3
5Admin access requests: approve, reject, bulk approve, bulk reject, plus counts (§7)M3
9Admin users: list, create, edit, resend link, password reset, deactivate, reactivate, super-admin, deleteM3
1GET /admin/audit-logM3
5Me: get, patch, delete, exchange rates (2)M3, M4
4Sync: sync-state, import, push, pullM4
12Spaces, members, invitationsM5
3TransfersM5

7. Where the prototypes and the documents disagree ​

Each of these needs a decision before the endpoint is built. None is a mistake in the endpoint list; they are screens or fields the written requirements do not cover.

#Prototype showsDocuments saySuggested decision
1The request form has "How do you know me?" (required) and an "About yourself" (required, at least 10 characters)FR-REQ-1: name, email, country, and an optional message. The table access_request has no know columnAdd know (text, required) and keep message; or fold both into message. Then the DTO, the migration, the admin detail panel, and FR-REQ-1 all change together
2The admin Requests tabs show a count per tabThe built list returns no countsReturn counts: { pending, approved, rejected } from GET /admin/access-requests, or reuse the Overview call
3The admin list distinguishes an activation link that bouncedNo column records an email bounceAdd activation_email_failed_at on the user, set by the Brevo bounce webhook (§11.3), or drop the "bounced" state from the prototype
4The admin Settings has a color theme (accent)app_user.theme holds light, dark, or system onlyKeep the accent on the device (localStorage), as the prototype already does; no endpoint
5"Part of this is held for something else": money inside a financial account that belongs to someone else, shown as "Held for others" and kept out of net worth; a "Bali trip" shared space whose fund sits in Ayu's BCANot in the BRD, the journeys, or the data model. FR-SPC-17 says money in a private financial account is shared by creating a new financial account in the shared spaceDecide whether this is a new requirement (needs a table or a flag, and a journey) or a prototype idea to remove. No endpoint is planned for it
6Financial account type Other (investments, pay later)FR-FIN-1: cash, bank, e-wallet, credit card onlyAdd the type to the requirement and the type enum, or map "Other" onto one of the four
7Subcategories in onboarding, Categories, and StatsFR-CAT-3 is "Could"; the table has parent_id alreadyFine for sync; set the milestone for FR-CAT-3
8Savings goals "Already saved" (a starting amount) with no linked financial accountFR-BUD-4 to 6: linked account, or manual contributionsThe "already saved" amount is one goal_contribution row created with the goal
9The portal's Approve selected / Reject selectedFR-ADM-18 is "Could"Built into the endpoint list above because the screen is drawn

When one of these is decided, change the requirement first, then this document, then the code (BRD wins over everything else).

Crest is a personal project by Reizkian Y. Radityatama.