Skip to content

Tables ​

What each table is for, and the business requirements, business rules and user journeys it serves. This page is generated by pnpm docs:schema; the structure comes from the real schema, so it matches the migrations (Apps/Api/Migrations).

access_request ​

A visitor's request to use Crest, from submission to approval or rejection. No user exists until a Super Admin approves it.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
full_nametextno
emailcitextno
countrytextno
messagetextyes
consent_attimestamp with time zoneno
statusaccess_request_statusno'Pending'::access_request_status
rejection_notetextyes
user_iduuidyes
source_ip_hashtextyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • access_request_country_check: check, CHECK ((country ~ '^[A-Z]{2}$'::text))
  • access_request_full_name_check: check, CHECK ((length(TRIM(BOTH FROM full_name)) > 0))
  • access_request_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • access_request_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • access_request_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE SET NULL
  • access_request_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE UNIQUE INDEX access_request_pending_email_idx ON public.access_request USING btree (email) WHERE ((status = 'Pending'::access_request_status) AND (deleted_at IS NULL))
  • CREATE INDEX access_request_status_idx ON public.access_request USING btree (status, created_at DESC)

Relations

app_useriduuidaccess_requestiduuidfull_nametextemailcitextcountrytextmessagetextconsent_attimestamp with time zonestatusaccess_request_statusrejection_notetextuser_iduuidsource_ip_hashtextcreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-REQ-1 Anyone can submit an access request with full name, email, country, and optional reason/message
  • FR-REQ-1a The request form is a page on the public website with its own link (to share with friends), also reachable from the app's Connect screen (More → Conn…
  • FR-REQ-2 The request form is protected against spam (rate limiting and bot protection)
  • FR-REQ-3 The requester sees a confirmation that the request was received; no user is created at this point
  • FR-REQ-4 A duplicate request for an email that is already pending or active is not created; the requester sees a neutral message that does not reveal whether…
  • FR-REQ-5 The requester receives an email when their request is approved (with activation link)
  • FR-REQ-6 The request form links to the Privacy Policy and Terms of Use (in the visitor's language); submitting requires accepting both and confirming the visi…
  • FR-ADM-4 Super Admin sees a list of access requests, filterable by status (Pending, Approved, Rejected)
  • FR-ADM-5 Super Admin can approve a request, which creates the user and sends the activation link
  • FR-ADM-6 Super Admin can reject a request, with an optional reason
  • FR-ADM-6a Super Admin can approve a request that was previously rejected (to reverse a rejection); this works like a normal approval
  • FR-ADM-7a If the email in FR-ADM-7 already has a Pending request, that request is approved instead of creating a separate user
  • FR-ADM-13 Super Admin is notified (email) when a new access request arrives
  • FR-ADM-18 Super Admin can select several access requests and approve or reject them together
  • NFR-3 Privacy
  • NFR-18 Data retention

Business rules

  • BR-14 A rejected requester may submit a new request after 30 days
  • BR-44 Crest users must be 18 or older, confirmed on the first launch of the app (local mode) and again on the access-request form

User journeys

  • UJ-01 Request access
  • UJ-02 Try to register or log in without approval
  • UJ-09 Review and approve an access request
  • UJ-10 Reject an access request
  • UJ-11 Create a user directly

app_user ​

One row per person who can log in: identity, status, preferences, and the Super Admin flag.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
nametextno
emailcitextno
email_verifiedbooleannofalse
pending_emailcitextyes
password_hashtextyes
statususer_statusno'Invited'::user_status
is_super_adminbooleannofalse
base_currencytextno'IDR'::text
languagetextno'en'::text
time_zonetextno'Asia/Jakarta'::text
themetheme_preferenceno'System'::theme_preference
daily_reminder_timetime without time zoneyes
pin_hashtextyes
last_login_attimestamp with time zoneyes
paid_untildateyes
contribution_exemptbooleannofalse
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • app_user_base_currency_check: check, CHECK ((base_currency ~ '^[A-Z]{3}$'::text))
  • app_user_language_check: check, CHECK ((language = ANY (ARRAY['en'::text, 'id'::text])))
  • app_user_name_check: check, CHECK ((length(TRIM(BOTH FROM name)) > 0))
  • app_user_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • app_user_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • app_user_pkey: primary key, PRIMARY KEY (id)
  • app_user_email_key: unique, UNIQUE (email)

Relations

access_requestcreated_byuuidupdated_byuuiduser_iduuidaudit_logcreated_byuuidauth_sessioncreated_byuuidupdated_byuuiduser_iduuidauth_tokencreated_byuuidupdated_byuuiduser_iduuidauth_totpcreated_byuuidupdated_byuuiduser_iduuidbudgetcreated_byuuidupdated_byuuidcategorycreated_byuuidupdated_byuuidcontribution_paymentcreated_byuuidupdated_byuuiduser_iduuiddevice_push_tokencreated_byuuidupdated_byuuiduser_iduuidexchange_ratecreated_byuuidupdated_byuuiduser_iduuidfinancial_accountcreated_byuuidupdated_byuuidgoal_contributioncreated_byuuidupdated_byuuidrecurrencecreated_byuuidupdated_byuuidsavings_goalcreated_byuuidupdated_byuuidspacecreated_byuuidupdated_byuuidspace_invitationcreated_byuuidinvitee_user_iduuidupdated_byuuidspace_membercreated_byuuidupdated_byuuiduser_iduuidtransfercreated_byuuidupdated_byuuidtxncreated_byuuidupdated_byuuidapp_useriduuidnametextemailcitextemail_verifiedbooleanpending_emailcitextpassword_hashtextstatususer_statusis_super_adminbooleanbase_currencytextlanguagetexttime_zonetextthemetheme_preferencedaily_reminder_timetime without time zonepin_hashtextlast_login_attimestamp with time zonepaid_untildatecontribution_exemptbooleancreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-AUTH-1 There is no public sign-up for connected mode
  • FR-AUTH-2 An approved user activates their login through a single-use activation link and sets a password
  • FR-AUTH-4 Users log in with email and password
  • FR-AUTH-6 Users can lock Crest with a PIN
  • FR-AUTH-8 Users can delete themselves (their login) and all their data
  • FR-AUTH-9 A deactivated or deleted user is logged out of all devices immediately
  • FR-ADM-1 Only users with the Super Admin role can access the portal
  • FR-ADM-7 Super Admin can create a user directly (without a request), which sends an activation link
  • FR-ADM-8 Super Admin sees a list of all users with name, email, status, created date, and last login; searchable and sortable
  • FR-ADM-9 Super Admin can deactivate and reactivate a user
  • FR-ADM-11 Super Admin can permanently delete a user and all their data, after typed confirmation
  • FR-ADM-12 Super Admin can edit a user's name and email
  • FR-ADM-16 Super Admin can give the Super Admin role to any Active user, or remove it (never from the last Super Admin)
  • FR-SET-1 Users can choose their base currency (any ISO 4217 currency; also the currency of their Personal space) and language (English, Bahasa Indonesia at la…
  • FR-SET-2 Users can choose light/dark theme
  • FR-NOT-1 Optional daily reminder to log transactions

Business rules

  • BR-8 A user can only be created by the Super Admin, either by approving an access request or by creating it directly
  • BR-9 One email address maps to at most one user
  • BR-11 Deactivating a user keeps their data; only deletion removes it
  • BR-12 The Super Admin role manages access only; it grants no access to users' financial data
  • BR-13 There must always be at least one active Super Admin; the last one cannot be deactivated, deleted, or demoted
  • BR-41 Super Admin is a role held by a user

User journeys

  • UJ-03 Activate login from invitation
  • UJ-04 Log in and unlock Crest
  • UJ-07 Get logged out after deactivation
  • UJ-08 Delete myself and all my data
  • UJ-09 Review and approve an access request
  • UJ-11 Create a user directly
  • UJ-12 Deactivate and reactivate a user
  • UJ-13 Permanently delete a user
  • UJ-14 Help a user who cannot log in
  • UJ-15 First-time portal setup, daily check, and audit review
  • UJ-16 First-time setup
  • UJ-37 Change preferences and reminders

app_user_public ​

A view of only the id and name of the people you share a space with, so the app can show "Added by Jojo" without exposing anyone's profile.

Columns

ColumnTypeNullableDefault
iduuidyes
nametextyes

Business requirements

  • FR-SPC-7 Every transaction shows who added it and, if changed, who last edited it

Business rules

  • BR-30 Every financial account, category, budget, and savings goal belongs to exactly one space
  • BR-35 In a transfer between spaces, members see only the side in their own space

User journeys

  • UJ-44 Add a transaction in a shared space
  • UJ-45 Top up a shared fund from a Personal financial account
  • UJ-46 See how a shared space's money is used
  • UJ-47 Pay yourself back from a shared fund

audit_log ​

Append-only record of every Super Admin action. It never holds financial data or space details.

Columns

ColumnTypeNullableDefault
idbigintno
actiontextno
target_user_iduuidyes
target_request_iduuidyes
detailsjsonbno'{}'::jsonb
ip_hashtextyes
created_attimestamp with time zonenonow()
created_byuuidyes

Constraints

  • audit_log_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • audit_log_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX audit_log_created_idx ON public.audit_log USING btree (created_at DESC)

Relations

app_useriduuidaudit_logidbigintactiontexttarget_user_iduuidtarget_request_iduuiddetailsjsonbip_hashtextcreated_attimestamp with time zonecreated_byuuid

Business requirements

  • FR-ADM-3a As a last resort, the setup script can reset a Super Admin's 2FA (requires server access); the reset is recorded in the audit log
  • FR-ADM-14 Every admin action is recorded in an audit log (who, what, which user, when), viewable in the portal
  • FR-ADM-16 Super Admin can give the Super Admin role to any Active user, or remove it (never from the last Super Admin)
  • FR-ADM-17 Super Admin cannot view users' financial data (financial accounts, transactions, budgets) or anything about their spaces, including space names and m…
  • NFR-12 Audit
  • NFR-18 Data retention

Business rules

None.

User journeys

  • UJ-09 Review and approve an access request
  • UJ-10 Reject an access request
  • UJ-11 Create a user directly
  • UJ-12 Deactivate and reactivate a user
  • UJ-13 Permanently delete a user
  • UJ-14 Help a user who cannot log in
  • UJ-15 First-time portal setup, daily check, and audit review

auth_session ​

One row per logged-in device. Deleting or revoking a row logs that device out.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
user_iduuidno
clientauth_clientno
refresh_token_hashtextno
device_nametextyes
two_factor_verifiedbooleannofalse
last_used_attimestamp with time zonenonow()
expires_attimestamp with time zoneno
revoked_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • auth_session_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • auth_session_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • auth_session_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • auth_session_pkey: primary key, PRIMARY KEY (id)
  • auth_session_refresh_token_hash_key: unique, UNIQUE (refresh_token_hash)

Indexes

  • CREATE INDEX auth_session_user_idx ON public.auth_session USING btree (user_id)

Relations

app_useriduuidauth_sessioniduuiduser_iduuidclientauth_clientrefresh_token_hashtextdevice_nametexttwo_factor_verifiedbooleanlast_used_attimestamp with time zoneexpires_attimestamp with time zonerevoked_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-AUTH-4 Users log in with email and password
  • FR-AUTH-9 A deactivated or deleted user is logged out of all devices immediately
  • FR-ADM-2 Super Admin login requires two-factor authentication (authenticator app)
  • NFR-2 Security
  • NFR-11 Admin security

Business rules

None.

User journeys

  • UJ-04 Log in and unlock Crest
  • UJ-07 Get logged out after deactivation
  • UJ-12 Deactivate and reactivate a user
  • UJ-13 Permanently delete a user

auth_token ​

Single-use links sent by email: login activation, password reset, and email change.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
user_iduuidno
purposeauth_token_purposeno
token_hashtextno
expires_attimestamp with time zoneno
used_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • auth_token_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • auth_token_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • auth_token_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • auth_token_pkey: primary key, PRIMARY KEY (id)
  • auth_token_token_hash_key: unique, UNIQUE (token_hash)

Indexes

  • CREATE INDEX auth_token_user_idx ON public.auth_token USING btree (user_id, purpose)

Relations

app_useriduuidauth_tokeniduuiduser_iduuidpurposeauth_token_purposetoken_hashtextexpires_attimestamp with time zoneused_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-AUTH-2 An approved user activates their login through a single-use activation link and sets a password
  • FR-AUTH-3 Activation links expire after 72 hours; the Super Admin can resend a new link
  • FR-AUTH-7 Users can reset their password
  • FR-ADM-10 Super Admin can resend an activation link or trigger a password-reset email for a user
  • FR-ADM-12 Super Admin can edit a user's name and email
  • FR-APP-7 Activating a login and resetting a password happen on the public website, so they work before the user has ever opened the app

Business rules

  • BR-10 Activation links are single-use and expire after 72 hours

User journeys

  • UJ-03 Activate login from invitation
  • UJ-05 Reset a forgotten password
  • UJ-09 Review and approve an access request
  • UJ-11 Create a user directly
  • UJ-14 Help a user who cannot log in

auth_totp ​

Two-factor authenticator secret and backup codes for Super Admins.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
user_iduuidno
secret_encryptedtextno
confirmed_attimestamp with time zoneyes
backup_code_hashestext[]no'{}'::text[]
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • auth_totp_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • auth_totp_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • auth_totp_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • auth_totp_pkey: primary key, PRIMARY KEY (id)
  • auth_totp_user_id_key: unique, UNIQUE (user_id)

Relations

app_useriduuidauth_totpiduuiduser_iduuidsecret_encryptedtextconfirmed_attimestamp with time zonebackup_code_hashestext[]created_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-ADM-2 Super Admin login requires two-factor authentication (authenticator app)
  • FR-ADM-2a When setting up 2FA, the Super Admin receives one-time backup codes to log in if the authenticator device is lost
  • FR-ADM-3a As a last resort, the setup script can reset a Super Admin's 2FA (requires server access); the reset is recorded in the audit log
  • NFR-11 Admin security

Business rules

None.

User journeys

  • UJ-15 First-time portal setup, daily check, and audit review

budget ​

A monthly spending limit per category, in the space's currency. The latest row on or before a period applies.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
category_iduuidno
amountbigintno
effective_fromdateno
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • budget_amount_check: check, CHECK ((amount > 0))
  • budget_category_id_fkey: foreign key, FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE
  • budget_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • budget_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • budget_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • budget_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX budget_space_idx ON public.budget USING btree (space_id, category_id, effective_from DESC)

Relations

spaceiduuidcategoryiduuidapp_useriduuidbudgetiduuidspace_iduuidcategory_iduuidamountbiginteffective_fromdatecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-BUD-1 The space's Owners can set a monthly budget per category, in the space's currency
  • FR-BUD-2 Users see progress against each budget (spent / remaining)
  • FR-BUD-3 Members of the space get notified at 80% and 100% of a budget
  • FR-BUD-7 Expenses in currencies other than the space's currency count toward budgets after conversion with the viewing user's saved rate; if the rate is missi…
  • FR-BUD-8 A budget on a category with sub-categories includes the spending in its sub-categories
  • FR-NOT-2 Budget threshold alerts (see FR-BUD-3)

Business rules

  • BR-4 Budget periods start on the space's configured start day (default: 1st of month)
  • BR-22 Budgets are always set in the space's currency
  • BR-24 A refund reduces spending in its category (and the related budget)

User journeys

  • UJ-27 Set monthly budgets
  • UJ-28 Get a budget alert and react
  • UJ-30 Review spending in Stats

category ​

Income and expense categories of a space, seeded with defaults when the space is created.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
parent_iduuidyes
kindcategory_kindno
nametextno
colortextyes
icontextyes
system_keytextyes
sort_orderintegerno0
archived_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • category_name_check: check, CHECK ((length(TRIM(BOTH FROM name)) > 0))
  • category_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • category_parent_id_fkey: foreign key, FOREIGN KEY (parent_id) REFERENCES category(id) ON DELETE CASCADE
  • category_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • category_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • category_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX category_space_idx ON public.category USING btree (space_id)
  • CREATE UNIQUE INDEX category_system_key_idx ON public.category USING btree (space_id, system_key) WHERE (system_key IS NOT NULL)

Relations

spaceiduuidapp_useriduuidbudgetcategory_iduuidtxncategory_iduuidcategoryiduuidspace_iduuidparent_iduuidkindcategory_kindnametextcolortexticontextsystem_keytextsort_orderintegerarchived_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-CAT-1 Every space starts with a default set of income and expense categories, named in the language of the person who created the space
  • FR-CAT-2 The space's Owners can create, rename, recolor, and archive its categories
  • FR-CAT-3 Categories can have sub-categories
  • FR-CAT-4 A transaction's category always comes from the space of its financial account: when adding a transaction, the category picker shows that space's cate…
  • FR-TRX-2e On any transfer (same or different currency) the user can optionally record a fee, e.g

Business rules

  • BR-5 Deleting a category does not delete its transactions; they become "Uncategorized"
  • BR-30 Every financial account, category, budget, and savings goal belongs to exactly one space
  • BR-31 A transaction's category must belong to the same space as its financial account

User journeys

  • UJ-16 First-time setup
  • UJ-17 Log a daily expense
  • UJ-34 Manage categories
  • UJ-42 Create a shared space
  • UJ-44 Add a transaction in a shared space

contribution_payment ​

A yearly contribution recorded by the Super Admin, which moves a user's paid-until date. Phase 4, reserved.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
user_iduuidno
paid_untildateno
notetextyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • contribution_payment_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • contribution_payment_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • contribution_payment_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • contribution_payment_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX contribution_payment_user_idx ON public.contribution_payment USING btree (user_id)

Relations

app_useriduuidcontribution_paymentiduuiduser_iduuidpaid_untildatenotetextcreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-SUB-1 Super Admin can set a paid-until date for each user, and see who is paid, due soon, or lapsed
  • FR-SUB-2 Super Admin can mark a user as exempt (e.g
  • FR-SUB-7 Every change to paid-until or exempt status is recorded in the audit log

Business rules

  • BR-27 Crest never processes payments
  • BR-28 A user whose contribution has lapsed always keeps read and export access to their own data and their spaces
  • BR-29 The contribution amount is set only to cover Crest's running costs, not to make a profit

User journeys

  • UJ-40 Record a yearly contribution
  • UJ-41 Contribution comes due, lapses, and is renewed

device_push_token ​

A device's push notification subscription, used for reminders and alerts. Phase 2.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
user_iduuidno
tokentextno
platformdevice_platformno
device_nametextyes
last_success_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • device_push_token_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • device_push_token_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • device_push_token_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • device_push_token_pkey: primary key, PRIMARY KEY (id)
  • device_push_token_token_key: unique, UNIQUE (token)

Indexes

  • CREATE INDEX device_push_token_user_idx ON public.device_push_token USING btree (user_id)

Relations

app_useriduuiddevice_push_tokeniduuiduser_iduuidtokentextplatformdevice_platformdevice_nametextlast_success_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-NOT-1 Optional daily reminder to log transactions
  • FR-NOT-2 Budget threshold alerts (see FR-BUD-3)
  • FR-NOT-3 Notifications need connected mode
  • FR-APP-3 When browser push notifications are introduced, the app asks once for permission, explaining what they are for
  • FR-SPC-16 Each member can turn on Notify me when someone adds a transaction for a shared space (off by default)

Business rules

None.

User journeys

  • UJ-28 Get a budget alert and react
  • UJ-37 Change preferences and reminders
  • UJ-38 Add Crest to the home screen

exchange_rate ​

A user's own manually entered rate from a currency to their base currency. It is private to that user and used only for converted totals.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
user_iduuidno
currencytextno
rate_to_basenumeric(24,12)no
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • exchange_rate_currency_check: check, CHECK ((currency ~ '^[A-Z]{3}$'::text))
  • exchange_rate_rate_to_base_check: check, CHECK ((rate_to_base > (0)::numeric))
  • exchange_rate_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • exchange_rate_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • exchange_rate_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • exchange_rate_pkey: primary key, PRIMARY KEY (id)
  • exchange_rate_user_id_currency_key: unique, UNIQUE (user_id, currency)

Relations

app_useriduuidexchange_rateiduuiduser_iduuidcurrencytextrate_to_basenumeric(24,12)created_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-FIN-6 Users can maintain their own exchange rates (one rate per currency to their base currency) in settings, with the date each rate was last updated
  • FR-FIN-7 Totals across currencies (net worth, dashboard) are converted into the base currency using the user's saved rates; the app shows the rate date and pr…
  • FR-TRX-2d The rate field is pre-filled with the user's saved rate for that currency pair (FR-FIN-6), if one exists; the user can override it for this transfer…
  • FR-BUD-7 Expenses in currencies other than the space's currency count toward budgets after conversion with the viewing user's saved rate; if the rate is missi…

Business rules

  • BR-15 Exchange rates are always entered manually by the user; Crest does not fetch rates from external services
  • BR-16 A cross-currency transfer stores the amount sent, the amount received, and the rate used
  • BR-17 Converted totals are estimates for display only; financial account balances always stay in that financial account's own currency

User journeys

  • UJ-22 Transfer between currencies with a manual rate
  • UJ-31 Check net worth across currencies
  • UJ-35 Maintain exchange rates

financial_account ​

A place money lives: cash, bank, e-wallet, or credit card, with one currency. Its balance is derived from its transactions.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
nametextno
typefinancial_account_typeno
currencytextno
opening_balancebigintno0
providertextyes
colortextyes
icontextyes
last4textyes
credit_limitbigintyes
statement_daysmallintyes
due_daysmallintyes
sort_orderintegerno0
archived_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • financial_account_check: check, CHECK (((type = 'CreditCard'::financial_account_type) OR ((credit_limit IS NULL) AND (statement_day IS NULL) AND (due_day IS NULL))))
  • financial_account_credit_limit_check: check, CHECK ((credit_limit >= 0))
  • financial_account_currency_check: check, CHECK ((currency ~ '^[A-Z]{3}$'::text))
  • financial_account_due_day_check: check, CHECK (((due_day >= 1) AND (due_day <= 31)))
  • financial_account_last4_check: check, CHECK ((last4 ~ '^[0-9]{4}$'::text))
  • financial_account_name_check: check, CHECK ((length(TRIM(BOTH FROM name)) > 0))
  • financial_account_statement_day_check: check, CHECK (((statement_day >= 1) AND (statement_day <= 31)))
  • financial_account_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • financial_account_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • financial_account_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • financial_account_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX financial_account_space_idx ON public.financial_account USING btree (space_id)

Relations

spaceiduuidapp_useriduuidsavings_goalfinancial_account_iduuidtxnfinancial_account_iduuidfinancial_accountiduuidspace_iduuidnametexttypefinancial_account_typecurrencytextopening_balancebigintprovidertextcolortexticontextlast4textcredit_limitbigintstatement_daysmallintdue_daysmallintsort_orderintegerarchived_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-FIN-1 Users can create any number of financial accounts, each with a name, type (cash, bank, e-wallet / online payment, credit card), currency, and opening…
  • FR-FIN-2 Users can edit, archive, and reorder financial accounts
  • FR-FIN-3 The app shows each financial account's current balance, derived from its transactions
  • FR-FIN-4 Each space shows its net worth: assets (cash, bank, e-wallet) minus liabilities (credit cards)
  • FR-FIN-5 Users can hold financial accounts in different currencies
  • FR-FIN-8 Users can optionally add the provider name (e.g
  • FR-FIN-9 Financial accounts are grouped by type on the Financial Accounts screen, with a subtotal per group
  • FR-FIN-10 Credit card financial accounts show the amount owed; users can optionally set a credit limit and see available credit
  • FR-FIN-10a Unusual balances are allowed but highlighted: a cash / bank / e-wallet balance below zero, a credit card over its limit, and a credit card paid more…
  • FR-FIN-11 Credit card financial accounts can optionally have a statement day and payment due day, with a reminder to the space's Owners before the due date
  • FR-FIN-14 A financial account's currency can be changed only while it has no transactions; after that the field is locked
  • FR-FIN-15 Before a financial account is deleted, the warning lists the other financial accounts it has transfers with, and explains that those transfers become…
  • FR-SPC-3 Every financial account, category, budget, and savings goal belongs to exactly one space
  • FR-SPC-17 Financial accounts can't be moved between spaces

Business rules

  • BR-1 Each financial account has exactly one currency; transactions inherit the financial account's currency
  • BR-3 Financial account balance = opening balance + sum of all its transactions
  • BR-6 Deleting a financial account requires confirmation and deletes its transactions; archiving keeps them
  • BR-19 Crest stores at most the last 4 digits of any card or bank account number, never the full number, CVV, PIN, or login details
  • BR-23 Crest records what really happened: negative balances, spending over a credit limit, and overpaid credit cards are allowed, never blocked
  • BR-26 A financial account's currency is fixed once it has any transaction
  • BR-30 Every financial account, category, budget, and savings goal belongs to exactly one space
  • BR-37 Financial accounts can't be moved between spaces

User journeys

  • UJ-16 First-time setup
  • UJ-20 Pay with a credit card
  • UJ-21 Pay a credit card bill
  • UJ-31 Check net worth across currencies
  • UJ-32 Add and organize financial accounts
  • UJ-33 Archive or delete a financial account

goal_contribution ​

A manually recorded contribution to a savings goal that is not linked to a financial account.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
goal_iduuidno
amountbigintno
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • goal_contribution_amount_check: check, CHECK ((amount <> 0))
  • goal_contribution_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • goal_contribution_goal_id_fkey: foreign key, FOREIGN KEY (goal_id) REFERENCES savings_goal(id) ON DELETE CASCADE
  • goal_contribution_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • goal_contribution_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX goal_contribution_goal_idx ON public.goal_contribution USING btree (goal_id)

Relations

savings_goaliduuidapp_useriduuidgoal_contributioniduuidgoal_iduuidamountbigintcreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-BUD-6 If no financial account is linked, the user records contributions to the goal manually

Business rules

None.

User journeys

  • UJ-29 Create and track a savings goal

recurrence ​

A repeating transaction (daily, weekly, monthly, yearly) that the server creates on its due date.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
templatejsonbno
frequencyrecurrence_frequencyno
everysmallintno1
anchor_daysmallintyes
start_ondateno
end_ondateyes
next_run_ondateyes
stopped_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • recurrence_anchor_day_check: check, CHECK (((anchor_day >= 1) AND (anchor_day <= 31)))
  • recurrence_every_check: check, CHECK ((every >= 1))
  • recurrence_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • recurrence_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • recurrence_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • recurrence_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX recurrence_due_idx ON public.recurrence USING btree (next_run_on) WHERE ((stopped_at IS NULL) AND (deleted_at IS NULL))
  • CREATE INDEX recurrence_space_idx ON public.recurrence USING btree (space_id)

Relations

spaceiduuidapp_useriduuidtxnrecurrence_iduuidrecurrenceiduuidspace_iduuidtemplatejsonbfrequencyrecurrence_frequencyeverysmallintanchor_daysmallintstart_ondateend_ondatenext_run_ondatestopped_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-TRX-5 Users can set up recurring transactions (daily, weekly, monthly, yearly)
  • FR-TRX-5a When editing or deleting a transaction created by a recurrence, the user chooses Only this one or This and all future ones
  • FR-TRX-5b Recurring transactions are created by the server on their due date in connected mode, so occurrences are never missed while the phone is off or offli…
  • FR-TRX-5c A recurring transaction in a shared space belongs to the member who set it up; it stops if that member leaves the space

Business rules

  • BR-25 A recurrence on day 29, 30, or 31 falls on the last day of shorter months
  • BR-40 A recurring transaction in a shared space belongs to the member who set it up and stops when that member leaves the space

User journeys

  • UJ-26 Set up a recurring transaction
  • UJ-48 Leave a space or remove a member

savings_goal ​

A savings goal with a target amount and date, optionally linked to one financial account whose balance is the progress.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
nametextno
target_amountbigintno
target_datedateyes
financial_account_iduuidyes
completed_attimestamp with time zoneyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • savings_goal_name_check: check, CHECK ((length(TRIM(BOTH FROM name)) > 0))
  • savings_goal_target_amount_check: check, CHECK ((target_amount > 0))
  • savings_goal_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • savings_goal_financial_account_id_fkey: foreign key, FOREIGN KEY (financial_account_id) REFERENCES financial_account(id) ON DELETE SET NULL
  • savings_goal_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • savings_goal_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • savings_goal_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX savings_goal_space_idx ON public.savings_goal USING btree (space_id)

Relations

spaceiduuidfinancial_accountiduuidapp_useriduuidgoal_contributiongoal_iduuidsavings_goaliduuidspace_iduuidnametexttarget_amountbiginttarget_datedatefinancial_account_iduuidcompleted_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-BUD-4 The space's Owners can create savings goals with name, target amount, and target date
  • FR-BUD-5 A savings goal is linked to one financial account in the same space (e.g
  • FR-BUD-6 If no financial account is linked, the user records contributions to the goal manually

Business rules

None.

User journeys

  • UJ-29 Create and track a savings goal

schema_migration ​

Bookkeeping for the migration runner. Not a business table, so it has no requirements or journeys.

Columns

ColumnTypeNullableDefault
nametextno
checksumtextno
applied_attimestamp with time zonenonow()

Constraints

  • schema_migration_pkey: primary key, PRIMARY KEY (name)

Business requirements

None.

Business rules

None.

User journeys

None.

space ​

A Personal space (one per user) or a shared space. Every financial account, category, budget, and goal belongs to exactly one space.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
kindspace_kindno
nametextno
currencytextno
time_zonetextno
budget_start_daysmallintno1
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • space_budget_start_day_check: check, CHECK (((budget_start_day >= 1) AND (budget_start_day <= 28)))
  • space_currency_check: check, CHECK ((currency ~ '^[A-Z]{3}$'::text))
  • space_name_check: check, CHECK ((length(TRIM(BOTH FROM name)) > 0))
  • space_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • space_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • space_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE UNIQUE INDEX space_personal_owner_idx ON public.space USING btree (created_by) WHERE (kind = 'Personal'::space_kind)

Relations

app_useriduuidbudgetspace_iduuidcategoryspace_iduuidfinancial_accountspace_iduuidrecurrencespace_iduuidsavings_goalspace_iduuidspace_invitationspace_iduuidspace_memberspace_iduuidtxnspace_iduuidspaceiduuidkindspace_kindnametextcurrencytexttime_zonetextbudget_start_daysmallintcreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-SPC-1 Every user gets a Personal space automatically when their login is activated (connected mode; in local mode the Personal space is created on the devi…
  • FR-SPC-2 Any Active user can create a shared space with a name, currency, and budget start day, and becomes its Owner
  • FR-SPC-3 Every financial account, category, budget, and savings goal belongs to exactly one space
  • FR-SPC-10 Owners can delete a shared space after typed confirmation
  • FR-SPC-11 A space switcher at the top of Crest moves between spaces
  • FR-SPC-12 Each space has a currency used for its budgets and reports
  • FR-SET-3 Users can set the start day of the budget month for their Personal space (e.g
  • FR-SET-4 Changing the base currency (or a shared space's currency) shows a warning, then asks the user (or Owner) to review that space's budgets, pre-converte…

Business rules

  • BR-4 Budget periods start on the space's configured start day (default: 1st of month)
  • BR-30 Every financial account, category, budget, and savings goal belongs to exactly one space
  • BR-32 A Personal space always has exactly one member, its user

User journeys

  • UJ-16 First-time setup
  • UJ-27 Set monthly budgets
  • UJ-37 Change preferences and reminders
  • UJ-42 Create a shared space
  • UJ-49 Hand over ownership or delete a shared space

space_invitation ​

An Owner's invitation for someone to join a shared space. It expires after 14 days.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
emailcitextno
rolespace_roleno'Member'::space_role
invitee_user_iduuidyes
statusinvitation_statusno'Pending'::invitation_status
expires_attimestamp with time zoneno(now() + '14 days'::interval)
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • space_invitation_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • space_invitation_invitee_user_id_fkey: foreign key, FOREIGN KEY (invitee_user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • space_invitation_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • space_invitation_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • space_invitation_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX space_invitation_invitee_idx ON public.space_invitation USING btree (invitee_user_id) WHERE (status = 'Pending'::invitation_status)
  • CREATE INDEX space_invitation_space_idx ON public.space_invitation USING btree (space_id)

Relations

spaceiduuidapp_useriduuidspace_invitationiduuidspace_iduuidemailcitextrolespace_roleinvitee_user_iduuidstatusinvitation_statusexpires_attimestamp with time zonecreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-SPC-4 Owners can invite other people by email
  • FR-SPC-5 Invitations expire after 14 days
  • FR-SPC-6 Members have the role Owner or Member, with the permissions in the table above
  • FR-NOT-4 Users are notified of invitations to a shared space, of being removed from one, and of a shared space being deleted

Business rules

  • BR-39 Only Active users can join a shared space, and only by accepting an invitation, which expires after 14 days

User journeys

  • UJ-43 Invite someone and accept the invitation

space_member ​

Who belongs to a space and as what (Owner or Member), plus their per-space settings.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
user_iduuidno
rolespace_roleno
include_in_totalsbooleanno
notify_new_txnbooleannofalse
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes

Constraints

  • space_member_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • space_member_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • space_member_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • space_member_user_id_fkey: foreign key, FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • space_member_pkey: primary key, PRIMARY KEY (id)
  • space_member_space_id_user_id_key: unique, UNIQUE (space_id, user_id)

Indexes

  • CREATE INDEX space_member_user_idx ON public.space_member USING btree (user_id)

Relations

spaceiduuidapp_useriduuidspace_memberiduuidspace_iduuiduser_iduuidrolespace_roleinclude_in_totalsbooleannotify_new_txnbooleancreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuid

Business requirements

  • FR-SPC-6 Members have the role Owner or Member, with the permissions in the table above
  • FR-SPC-8 A member can leave a shared space, and Owners can remove members (including other Owners, FR-SPC-6)
  • FR-SPC-9 The last Owner can't leave until they make someone else Owner or delete the space
  • FR-SPC-13 Each member has an Include in my totals setting per shared space: on by default for Owners, off by default for Members
  • FR-SPC-16 Each member can turn on Notify me when someone adds a transaction for a shared space (off by default)
  • FR-NOT-4 Users are notified of invitations to a shared space, of being removed from one, and of a shared space being deleted

Business rules

  • BR-32 A Personal space always has exactly one member, its user
  • BR-33 Every shared space has at least one Owner
  • BR-34 When a member leaves or is removed, transactions they added stay in the space with their name
  • BR-39 Only Active users can join a shared space, and only by accepting an invitation, which expires after 14 days
  • BR-43 Deactivating a user does not change their space memberships

User journeys

  • UJ-08 Delete myself and all my data
  • UJ-31 Check net worth across currencies
  • UJ-42 Create a shared space
  • UJ-43 Invite someone and accept the invitation
  • UJ-44 Add a transaction in a shared space
  • UJ-48 Leave a space or remove a member
  • UJ-49 Hand over ownership or delete a shared space

transfer ​

Links the two legs of a transfer between financial accounts, in the same or different spaces, and holds the exchange rate used.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
ratenumeric(24,12)yes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • transfer_rate_check: check, CHECK ((rate > (0)::numeric))
  • transfer_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • transfer_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • transfer_pkey: primary key, PRIMARY KEY (id)

Relations

app_useriduuidtxnfee_for_transfer_iduuidtransfer_iduuidtransferiduuidratenumeric(24,12)created_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-TRX-2 Users can record a transfer between financial accounts they can access, in the same space or in different spaces (FR-SPC-14)
  • FR-TRX-2a Users can transfer between financial accounts in different currencies (e.g
  • FR-TRX-2b In a cross-currency transfer the user enters the amount sent plus either the exchange rate or the amount received; the app calculates the missing val…
  • FR-TRX-2c The user can edit the calculated amount received to match what actually arrived (e.g
  • FR-TRX-2d The rate field is pre-filled with the user's saved rate for that currency pair (FR-FIN-6), if one exists; the user can override it for this transfer…
  • FR-TRX-2e On any transfer (same or different currency) the user can optionally record a fee, e.g
  • FR-TRX-3a Editing a cross-currency transfer uses the same form as creating one: changing any of amount sent, rate, or amount received recalculates the third
  • FR-FIN-12 Paying a credit card bill is recorded as a transfer from a bank / e-wallet / cash financial account to the credit card financial account
  • FR-FIN-13 A top-up (e.g
  • FR-SPC-14 A user can transfer money between financial accounts in any spaces they belong to (e.g

Business rules

  • BR-2 Transfers between financial accounts, in the same space or across spaces, do not count as income or expense
  • BR-15 Exchange rates are always entered manually by the user; Crest does not fetch rates from external services
  • BR-16 A cross-currency transfer stores the amount sent, the amount received, and the rate used
  • BR-35 In a transfer between spaces, members see only the side in their own space
  • BR-38 Requests for money between members (e.g

User journeys

  • UJ-19 Top up an e-wallet or withdraw cash
  • UJ-21 Pay a credit card bill
  • UJ-22 Transfer between currencies with a manual rate
  • UJ-45 Top up a shared fund from a Personal financial account
  • UJ-47 Pay yourself back from a shared fund

txn ​

Every expense, income, refund, and transfer leg. Its created_at is the transaction's date, and its amount is the signed effect on the financial account's balance.

Columns

ColumnTypeNullableDefault
iduuidnogen_random_uuid()
space_iduuidno
financial_account_iduuidno
kindtxn_kindno
amountbigintno
category_iduuidyes
notetextyes
transfer_iduuidyes
fee_for_transfer_iduuidyes
refund_of_txn_iduuidyes
recurrence_iduuidyes
counterparty_labeltextyes
receipt_pathtextyes
created_attimestamp with time zonenonow()
created_byuuidyes
updated_attimestamp with time zonenonow()
updated_byuuidyes
deleted_attimestamp with time zoneyes

Constraints

  • txn_check: check, CHECK ((((kind = 'Expense'::txn_kind) AND (amount < 0)) OR ((kind = ANY (ARRAY['Income'::txn_kind, 'Refund'::txn_kind])) AND (amount > 0)) OR ((kind = 'Transfer'::txn_kind) AND (amount <> 0))))
  • txn_check1: check, CHECK (((kind = 'Transfer'::txn_kind) = (transfer_id IS NOT NULL)))
  • txn_check2: check, CHECK (((kind <> 'Transfer'::txn_kind) OR (category_id IS NULL)))
  • txn_check3: check, CHECK (((fee_for_transfer_id IS NULL) OR (kind = 'Expense'::txn_kind)))
  • txn_check4: check, CHECK (((refund_of_txn_id IS NULL) OR (kind = 'Refund'::txn_kind)))
  • txn_category_id_fkey: foreign key, FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE SET NULL
  • txn_created_by_fkey: foreign key, FOREIGN KEY (created_by) REFERENCES app_user(id) ON DELETE SET NULL
  • txn_fee_for_transfer_id_fkey: foreign key, FOREIGN KEY (fee_for_transfer_id) REFERENCES transfer(id) ON DELETE SET NULL
  • txn_financial_account_id_fkey: foreign key, FOREIGN KEY (financial_account_id) REFERENCES financial_account(id) ON DELETE CASCADE
  • txn_recurrence_id_fkey: foreign key, FOREIGN KEY (recurrence_id) REFERENCES recurrence(id) ON DELETE SET NULL
  • txn_refund_of_txn_id_fkey: foreign key, FOREIGN KEY (refund_of_txn_id) REFERENCES txn(id) ON DELETE SET NULL
  • txn_space_id_fkey: foreign key, FOREIGN KEY (space_id) REFERENCES space(id) ON DELETE CASCADE
  • txn_transfer_id_fkey: foreign key, FOREIGN KEY (transfer_id) REFERENCES transfer(id) ON DELETE CASCADE
  • txn_updated_by_fkey: foreign key, FOREIGN KEY (updated_by) REFERENCES app_user(id) ON DELETE SET NULL
  • txn_pkey: primary key, PRIMARY KEY (id)

Indexes

  • CREATE INDEX txn_account_date_idx ON public.txn USING btree (financial_account_id, created_at)
  • CREATE INDEX txn_note_trgm_idx ON public.txn USING gin (note gin_trgm_ops)
  • CREATE INDEX txn_space_category_date_idx ON public.txn USING btree (space_id, category_id, created_at)
  • CREATE INDEX txn_space_creator_idx ON public.txn USING btree (space_id, created_by)
  • CREATE INDEX txn_space_date_idx ON public.txn USING btree (space_id, created_at)
  • CREATE INDEX txn_space_updated_idx ON public.txn USING btree (space_id, updated_at)
  • CREATE INDEX txn_transfer_idx ON public.txn USING btree (transfer_id) WHERE (transfer_id IS NOT NULL)

Relations

spaceiduuidfinancial_accountiduuidcategoryiduuidtransferiduuidrecurrenceiduuidapp_useriduuidtxniduuidspace_iduuidfinancial_account_iduuidkindtxn_kindamountbigintcategory_iduuidnotetexttransfer_iduuidfee_for_transfer_iduuidrefund_of_txn_iduuidrecurrence_iduuidcounterparty_labeltextreceipt_pathtextcreated_attimestamp with time zonecreated_byuuidupdated_attimestamp with time zoneupdated_byuuiddeleted_attimestamp with time zone

Business requirements

  • FR-TRX-1 Users can add an expense or income with amount, financial account, category, date and time (defaults to now; can be set to a past date for entries lo…
  • FR-TRX-2 Users can record a transfer between financial accounts they can access, in the same space or in different spaces (FR-SPC-14)
  • FR-TRX-3 Users can edit and delete transactions, within their role in the space (§8.4): Members only their own, Owners any
  • FR-TRX-4 Users can search and filter transactions in the current space by date, financial account, category, amount, and text
  • FR-TRX-5a When editing or deleting a transaction created by a recurrence, the user chooses Only this one or This and all future ones
  • FR-TRX-7 Users can always add, edit, and delete transactions without internet
  • FR-TRX-8 Users can record a refund: money returned for an earlier purchase, into any financial account, with an expense category (normally the one the origina…
  • FR-TRX-9 Future-dated transactions are shown in an Upcoming section of the transaction list
  • FR-SPC-7 Every transaction shows who added it and, if changed, who last edited it
  • FR-FIN-3 The app shows each financial account's current balance, derived from its transactions
  • FR-DAT-1 Users can export to CSV the transactions of any space they belong to (one space or all), shared through the device's share menu or downloaded
  • FR-DAT-2 Users can import transactions from CSV with column mapping
  • NFR-1 Accuracy

Business rules

  • BR-2 Transfers between financial accounts, in the same space or across spaces, do not count as income or expense
  • BR-3 Financial account balance = opening balance + sum of all its transactions
  • BR-6 Deleting a financial account requires confirmation and deletes its transactions; archiving keeps them
  • BR-18 Spending with a credit card is an expense when the purchase is made; paying the card bill is a transfer, so the same spending is not counted twice
  • BR-23 Crest records what really happened: negative balances, spending over a credit limit, and overpaid credit cards are allowed, never blocked
  • BR-24 A refund reduces spending in its category (and the related budget)
  • BR-31 A transaction's category must belong to the same space as its financial account
  • BR-34 When a member leaves or is removed, transactions they added stay in the space with their name
  • BR-35 In a transfer between spaces, members see only the side in their own space
  • BR-36 Members can edit or delete only transactions they added; Owners can edit or delete any transaction in their space
  • BR-42 Balances and net worth are always shown as of now

User journeys

  • UJ-17 Log a daily expense
  • UJ-18 Log income
  • UJ-19 Top up an e-wallet or withdraw cash
  • UJ-20 Pay with a credit card
  • UJ-21 Pay a credit card bill
  • UJ-22 Transfer between currencies with a manual rate
  • UJ-23 Fix or delete a transaction
  • UJ-24 Find a past transaction
  • UJ-25 Log a transaction without internet
  • UJ-36 Export and import transactions (CSV)
  • UJ-44 Add a transaction in a shared space
  • UJ-45 Top up a shared fund from a Personal financial account
  • UJ-46 See how a shared space's money is used
  • UJ-47 Pay yourself back from a shared fund
  • UJ-48 Leave a space or remove a member

Crest is a personal project by Reizkian Y. Radityatama.