Our work
We developed the company workspace for Chrono Pay, including wallet sign-in, onboarding, employee records, payroll preparation, tax estimates and invoices. The application keeps preparation, company approval and payment status separate so each amount and workflow state can be reviewed.
A payroll total is the end of several decisions. Someone selects the employees, checks their compensation, adds bonuses and adjustments, estimates taxes and chooses a payment date. If those decisions live in separate tools, the finance team has to reconstruct them before it can review the run. Chrono Pay brings employee records, payroll preparation, invoices and tax settings into one company workspace. Its architecture focuses on preserving the information behind each amount and making the status of the work visible.
The product brief introduced a second requirement that affected the entire application. A company needed to complete onboarding and submit verification information, but a pending review could not prevent its team from preparing payroll or creating invoices. That meant account access, onboarding completion and company approval needed separate meanings in both the backend and the interface. The main engineering problem was expressing those distinctions without turning the dashboard into a collection of blocked screens. Alongside it, the data model had to distinguish an employee's current compensation from the amounts recorded in a particular payroll run.
Company review could not stop preparatory work
A single account status would make the onboarding flow simpler to describe, but it would combine decisions that happen at different times. A person can prove control of a wallet before providing company information. They can complete the requested information before the company review is resolved. They can also prepare employee records while that review remains pending. Chrono Pay separates these states rather than asking one approval flag to govern every interaction.
The User model stores onboardingCompleted, while the Company model stores kycStatus. Newly created companies begin with the latter set to pending. First-login routing uses onboarding completion to choose between the setup flow and the dashboard. Submitting the application does not automatically change company verification to approved. The dashboard can therefore become available without implying that the company has passed review.
This distinction carries through to the available work. Employee creation, payroll drafts, invoice preparation and tax settings remain accessible during the pending state. Actions associated with money movement can display a verification requirement separately. A persistent verification banner explains the company's status without replacing the workspace. The practical consequence is that finance preparation and company review can proceed on their own timelines, while the interface keeps their outcomes distinct.
The five-step onboarding flow collects personal details, company information, ownership documents and tax settings before a final review. Separate REST endpoints handle the profile, company, documents and tax sections, followed by a submission endpoint. That structure gives each part of the process an explicit persistence boundary. The review screen brings the information back together before submission, so the user can inspect what the application will associate with the company.
A wallet signature establishes account access
Wallet authentication fits the product's Web3 audience, but the wallet is only the entry point to the application session. The client requests a challenge through /api/auth/nonce, the user signs the message and the backend verifies the signature through /api/auth/verify. The backend uses ethers for verification. Once the account is found or created, the session uses a JWT in an HttpOnly cookie. Dashboard APIs then operate behind authentication middleware.
The challenge has its own stored record. AuthNonce associates the nonce with a wallet address, an expiry time and a used flag. Expiration and one-time use prevent an old challenge from remaining a reusable sign-in credential. The sign-in flow verifies a wallet signature without requesting the private key or seed phrase. Signing the login message is also separate from authorising a transfer, which keeps the meaning of the authentication step narrow.
Company access requires another check after authentication. Operational records carry a companyId, and the company links back to its owner through ownerUserId. These relationships define company ownership separately from wallet authentication. A browser-supplied company identifier is not evidence that the authenticated user owns that company's records. Wallet signature verification establishes the account, while company scoping governs the records that account may access.
A payroll run needs its own compensation record
An employee profile describes current compensation. A payroll run describes the amounts selected for a particular period. Those values can diverge when a salary changes or a one-off adjustment is added. If a run referred only to today's employee profile, a later edit could change the context needed to understand an earlier calculation. Chrono Pay's payroll schema therefore stores line-level compensation values alongside the employee reference.
Each payroll item contains employeeId, baseSalary, bonus, adjustment and gross. The parent record holds the period, payment date, currency, gross amount, estimated tax and total amount. This gives the run its own calculation context without duplicating the entire employee profile. The employee reference preserves the relationship to the person, while the stored amounts describe that particular run. The schema provides a basis for reviewing historical calculations, although storing these fields alone does not make a record immutable.
The preparation flow follows the same structure. The user first selects the period, payment date and employees, then reviews compensation rows and edits bonuses or adjustments. The final step presents the gross payroll, estimated taxes, fees and total before scheduling. Each stage answers a different question before the run receives its scheduled status.
- The period and employee selection define the scope of the run.
- Base pay, bonuses and adjustments explain each employee's gross amount.
- The summary exposes estimated tax and the total for review.
- Scheduling records the intended payment date and workflow state.
Bonuses also have a separate model containing the employee, amount, currency and date. Its optional includedInPayrollId connects a bonus to the run that includes it. That link traces a one-off award to the payroll calculation that includes it. Duplicate-payment protection is a separate concern from this schema relationship.
Scheduling and settlement need different meanings
The payroll API provides a dedicated /api/payroll/:id/schedule action. Its defined effect is to persist a scheduled payroll record. The data model also includes later workflow labels, but a status label by itself does not prove that funds have moved. Keeping the scheduling action precise allows the case to explain payroll preparation without treating a database update as an external payment receipt.
Payout records have a separate responsibility. They hold recipient, amount, currency, method, reference and timestamps, with optional links to the employee and payroll run. This separates the employee's compensation settings from the payroll calculation and the payout record associated with it. The distinction matters when a finance user moves from a total to the underlying recipient detail. Each record answers a narrower question.
| Record | Stores | Purpose |
|---|---|---|
| Employee | Salary | Current terms |
| Bonus | Award | Extra pay |
| Payroll | Line amounts | Run review |
| Payout | Recipient | Payout detail |
| Invoice | Items | Billing |
| Tax settings | Rates | Estimates |
We kept currency explicit across salary records, payroll runs, bonuses, invoices and payouts. Each amount retains its own currency field. The workspace preserves that context when preparing and reviewing records; it does not combine different currencies into a single converted total.
Tax preparation needed visible assumptions
Tax settings belong to the company and include residency, VAT registration, payroll country, accounting currency and reporting frequency. The calculation service uses configured rates to produce estimates from gross payroll or taxable revenue. That gives the dashboard a clear relationship between an input, a rate and the resulting estimate. These figures are estimates based on the configured rates, rather than determined tax liabilities.
Preparation preferences are stored separately from those rates. autoPreparePayrollTax and autoPrepareVat record the company's chosen settings, while verification status remains its own state. Turning on a preparation preference does not resolve company review or establish that a tax payment has occurred. This repeats the same distinction used in onboarding: configuration, approval and execution describe different events. Keeping them separate makes the tax section consistent with the rest of the workspace.
Invoices needed to remain editable and reviewable
The invoice builder places an editor beside a live document preview. Its data model stores the client, issue and due dates, currency and item rows containing description, quantity, rate, tax rate and amount. Subtotal, tax and total belong to the invoice record. The preview exposes the result of the editing process before the user leaves the form, including company branding and recipient details. It is a practical review surface for the same billing information the application stores.
The document output stays within the browser's printing workflow through a dedicated print stylesheet. This matches the requirement for a printable invoice without adding a separate document-generation service to the architecture. Invoice workflow states remain separate from the document layout and calculations. The invoice record tracks application workflow. Printing it does not establish delivery or payment.
One API keeps the working model together
We built the frontend with React and TypeScript on Vite, backed by a single Node.js and Express API with MongoDB and Mongoose. REST routes follow the operational entities and their actions, so payroll scheduling sits beside payroll records and tax estimation sits beside tax settings. This keeps related preparation workflows within one backend. We kept these related workflows in one service so they share the company context and data model.
The frontend follows the same preference for a direct implementation. React Router separates onboarding and dashboard pages, a small Fetch wrapper handles API calls and custom CSS provides the interface system. The backend applies authentication rate limits, input checks, upload restrictions and filename sanitisation. Company documents retain metadata and a company association, while entity access remains subject to the authenticated company context. These controls support the application's core responsibility of keeping company work organised and properly scoped.
Chrono Pay's architecture separates preparation from approval and preserves the amounts behind each payroll run. That gives finance teams a workspace where they can review who is included, how the total was formed and which step remains pending without confusing those decisions with payment completion.
See our architecture in practice.
DEVLAB · ARCHITECTURE EXAMPLE
Agent
Commerce
A look inside the software architecture behind Agent Commerce.
View architecture