Settings: the five resources behind the surface
How five small resources carry the facts the rest of the app reads, and who can change each one
Folake renames her Surulere pharmacy. She opens Settings on her phone, taps Business profile, replaces the name, and saves. The dashboard header carries the new name on its next load, the account header in Settings shows it beneath her own, and the next invitation preview introduces the business by it. The edit touched one field on one screen; every surface that names the business reads the stored value.
The Settings surface #
Settings is one of the five product surfaces: the place a user manages account, business, team, support, and access concerns for the current business. The tab opens on a hub: an account header with the user's initials, name, role, business, and email, rendered from the sign-in context the app already holds, and a row per concern beneath it. Behind the rows sit five resources, each a small set of durable facts with its own screen and its own editor:
| Resource | Settings row | Editor |
|---|---|---|
| Personal Profile | Profile | every user, on the user's own account |
| Business Profile | Business profile | the owner, through manage_business_settings |
| Usage Scope | Use Tomero for | the owner, through manage_business_settings |
| Default Location | Default location | the owner, through manage_business_settings |
| Team Members | Team members | the owner, through manage_members |
The facts are read far beyond Settings. The dashboard header reads the business name and the Default Location's name. The greeting on Home reads the user's full name, and an invitation preview introduces the inviter by it. Navigation copy reads the Usage Scope. Every stock movement lands at a location, the Default Location by default. The team list decides who can act inside the business at all. The write rules are strict for the same reason: four of the five resources change through the owner alone, every settings write needs a connection, and two business facts stay fixed for the life of the business.
The hub renders from the cached context
- The hub reads the sign-in context from
GET /mealone: theuserblock carriesfull_name, the loginemailandphone_number, and the editablecontact_phone_number, and each membership row carries the business summary and the effective capability list. Opening Settings triggers zero extra fetches. - The account header's role label and business name derive from the active membership row.
Personal Profile (users) #
The Personal Profile is the authenticated user's own identity and contact facts, used for account display and communication. Two fields are editable:
full_name: the user's display name, trimmed, 1 to 120 characterscontact_phone_number: an optional Contact Phone Number, normalized under the same Nigerian phone rules the sign-in flow uses; clearing the field stores an explicit null
The edit is user-scoped: it takes the signed-in account alone, with zero business context, membership, or capability involved. This is the one settings write every user owns.
Three records can describe one person. The Personal Profile is the account: the user's own name and contact number, edited by the user. A membership joins that user to one business and carries the role, Job Title, and capability set; it changes through the owner in Team settings. An employee is a payroll record, a person the business may pay, and it may exist with zero app access. Each record has its own editor, so a name corrected on the Personal Profile leaves every employee record untouched.
A second boundary splits the phone numbers. A Login Identifier is a verified email or phone number the account signs in with; changing one is a security workflow, and it stays read-only through the profile route. The Contact Phone Number is display and contact data, editable at any time. The Profile form renders the login email read-only and points its phone field at the contact number alone.
Ngozi starts carrying a second SIM for work calls. She opens Settings, taps Profile, and types the new number into the phone field. The save sends the changed field alone, stores the number in +234 form, and refreshes the cached sign-in context on the way back. Her Contact Phone Number is now the new line; her Login Identifier stays the number she verified at registration, and she signs in exactly as before. The change emits one account-level user.profile_updated audit event listing the changed fields, and the event appears in zero business audit feeds: the profile is an account fact, and business feeds carry business-scoped events alone.
Profile edits on the wire
PATCH /me/profileacceptsfull_nameandcontact_phone_numberalone; unknown fields and an empty body fail with422 validation_error.full_nametrims and holds 1 to 120 characters; a nullfull_namefails validation.contact_phone_numbernormalizes to E.164 form and clears with an explicit null.- A submitted value equal to the stored one is accepted and returns the current profile facts. A real change emits the one
user.profile_updatedevent withchanged_fieldsmetadata; a no-op save emits nothing. - The login
emailandphone_number, the membership role, and business facts stay read-only through the route.
One stored name, two form fields
- The profile form shows First name and Surname; the backend stores a single
full_name. The app splits the stored name on the first space, the remainder joins the surname, and the two fields join with one space on save. - The phone field wears the local
80 0000 1234input mask and stores+234XXXXXXXXXX. - Save stays disabled until a changed-fields-only patch exists, the form sends changed fields alone, and server field errors map back to their inputs.
Business Profile (businesses) #
The Business Profile is the business's own identity facts: the name, the Business Type Label, the country, and the base currency. The owner edits three fields:
name: the business name, trimmed, 1 to 200 charactersbusiness_type_label: a free-form label for what the business does, trimmed, 1 to 120 charactersusage_scope: the Use Tomero for choice, edited on its own screen
The Business Type Label is copy customization only. The server stores the trimmed text, and clients read it for copy alone; the choose-a-type sheet in the app reuses the onboarding option list as a convenience, and a stored label outside that list still renders in the field.
Country and base currency are fixed at onboarding. The base currency defines the meaning of every money amount Tomero stores as integer minor units: payroll history, payslips, and inventory values all denominate in it, and an in-place change would reinterpret all of that history. The country code anchors statutory applicability the same way. The update schema omits both fields, so a request carrying either fails validation loudly instead of dropping it silently, and the app renders both as display-only fields. A genuine change is a future corrective workflow with its own audit and data semantics. The statutory profile, the statutory JSON the business supplied at onboarding, stays read-only at launch on every surface.
Business Profile writes sit behind manage_business_settings, one of the 6 Owner-only capabilities. Concurrency is last-write-wins: the save that lands last stands. An effective save emits one business-scoped business.updated audit event with the changed fields; a save that changes zero stored values commits nothing.
Business Profile on the wire
GET /businesses/{business_id}is the canonical read: any active member reads the full record. It is a pure read, with zero state change and zero audit event.PATCH /businesses/{business_id}sits behindmanage_business_settingsand takes at least one ofname,business_type_label, andusage_scope; an empty payload fails with422 validation_error, and unknown fields,country_code,base_currency, andstatutory_profileincluded, fail the same way.- A member without the capability answers
403 forbidden; a caller with zero membership in the business answers404 business_context_not_found. - The write runs online-only, without idempotency keys or version preconditions, and returns the full updated record.
- The mobile form seeds from the cached membership summary, keeps Save disabled until a changed-fields-only patch exists, and refreshes the cached context after a save.
Usage Scope (businesses.usage_scope) #
The Usage Scope records which launch surfaces the business chose to use: payroll_only, inventory_only, or payroll_and_inventory. Onboarding defaults it to payroll_and_inventory when the choice is omitted. In Settings it is the Use Tomero for screen: a radio list of the three values, seeded from the stored scope, with Save enabled once the selection differs.
The scope is a navigation and copy fact for clients, and that is its whole job. Server-side authorization reads the membership's capability set alone, so access to payroll and inventory routes follows grants, whatever the scope says. At launch the tab bar renders from capabilities too, so a scope change updates copy and leaves the tabs in place.
Usage Scope on the wire
- The Use Tomero for screen saves through the same
PATCH /businesses/{business_id}as the Business Profile, sendingusage_scopealone; the Business profile form leaves the field to this screen. - The same rules apply:
manage_business_settings, online-only, last-write-wins, onebusiness.updatedaudit event on an effective change. - The radio list seeds from the cached membership summary, and the cached context refreshes after a save.
Default Location (locations) #
The Default Location is the single main operating location every business runs from onboarding onward. Business creation writes it in the same transaction as the business itself, named from the onboarding form or "Main Location" when the name is omitted, and a partial unique index enforces at most one default location per business.
Two fields are editable:
name: the location's display name, trimmed, 1 to 120 charactersaddress: optional free text up to 255 characters; a blank or null value clears it
Any member reads the location; the write sits behind manage_business_settings. The record's other fields, kind, is_default, and is_active, sit outside the update schema, so the settings screen edits the display facts alone.
The location is read far beyond its settings screen. The dashboard header prints its name beside the business name. Every inventory threshold, movement, and balance row carries its id, and a stock movement recorded with an omitted location resolves to it; the inventory doc carries those consequences. Multi-location management is a deferred workflow, and the rows that would need it already carry a location_id, so more locations arrive as an addition.
Default Location on the wire
GET /default-locationandPATCH /default-locationform a singleton resource pair: the read is open to any active member, and the update requiresmanage_business_settings.- The PATCH takes at least one of
nameandaddress; an empty payload and unknown fields fail with422 validation_error. - An effective change emits one business-scoped
location.updatedaudit event withchanged_fields; a no-op save commits nothing. The write runs online-only, without idempotency keys, last-write-wins. - Onboarding always creates the location, so the routes'
404 default_location_not_foundanswer is defensive. - The location is absent from the cached sign-in context, so the mobile form seeds from the
GETon focus, with loading and retry states, and an emptied address field is sent as an explicitnull. The dashboard refetches the location name on focus, so the save skips the context refresh.
Team members on the surface #
The Team members row opens the merged list of members and pending invitations, and Invite team member starts the two-step invite flow. Both sit behind manage_members, so the owner alone opens them at launch. The team doc owns that whole surface: the list, member edits, the invitation lifecycle, and joining.
One boundary belongs here. A member's Job Title and capability set live on the membership and change through the owner in Team settings; the member's own name and Contact Phone Number live on the Personal Profile and change through the member. The two edits meet on the team list, where each row shows the member's full_name beside the owner-assigned Job Title.
Help & Support, Security, and Log out #
Three hub rows work with zero resources behind them.
Help & Support hands the conversation to the phone. Send an email opens the mail app with a prefilled subject, and Chat on WhatsApp opens a wa.me link, which falls back to the browser on a phone without WhatsApp. When the hand-off fails, on a phone with zero configured mail account for example, the screen shows the address and number in a warning card for manual contact.
Security is announced as disabled, with a Coming soon pill in place of its chevron. The row holds the place of the deferred Login Identifier workflow: changing a verified email or phone number is a security flow with its own verification, and it ships in a later slice.
Log out sits behind a confirmation dialog because signing out clears the stored session and resets the local database: queued unsynced changes are removed with it, and the dialog says so.
Launch boundaries #
The model is built so later workflows arrive as additions. The boundaries today:
- Every settings write needs a connection; the offline queue covers product creation and employee creation alone.
- Settings writes apply last-write-wins, without idempotency keys or version preconditions; the save that lands last stands.
- Business Profile, Usage Scope, and Default Location change through the owner alone:
manage_business_settingsis one of the 6 Owner-only capabilities at launch. - The Personal Profile is the one settings write a member owns; every business-scoped settings fact changes through the owner.
- Country and base currency are fixed at onboarding; a change is a future corrective workflow with its own audit and data semantics.
- The statutory profile stays read-only at launch.
- Changing a Login Identifier is a deferred security workflow; the Security row holds its place in the hub.
- Every business operates one Default Location; multi-location management is a deferred workflow.