| Brief type | Idea brief, written plan ready for execution |
| Product | Mobile and web app for trades businesses with one to ten people |
| Recommended route | Build a first version in three months, then a pilot with ten businesses |
| Our recommendation | Go, subject to the evidence tests in section 3 |
1Summary
Small trades businesses, such as plumbers, electricians, heating engineers and joiners, run most of their work from a phone, a notebook and a spreadsheet. Quotes are written in the evening, job details live in text messages, and invoices go out days after the work is done, which slows down cash flow for businesses that can least afford it.
The product we propose turns a customer enquiry into a quote, a booked visit, a completed job and a paid invoice, all from the phone, and works on site where mobile signal is poor. The office side, often a partner or a part time administrator, uses a web app to see the diary, chase quotes and check who has paid.
Established competitors already serve larger trades firms with broad feature sets. Our view is that there is room for a product aimed tightly at the sole trader and the small team, priced simply, quick to learn in an evening, and built around getting paid on the day. The first version should do that one journey well and leave everything else out.
We recommend going ahead with a three month build, provided the evidence tests below are passed in the first two weeks.
2Customer and problem
Who it is for
The first customer is an owner who works on the tools, employs up to four people, and handles quotes and invoices personally. A second user is the person who helps with the paperwork, often working part time from home.
The problem in their words
These are the problems we expect to hear in interviews. They are hypotheses until the interviews confirm them.
- Quotes take too long to write and look unprofessional when they are sent as a text message.
- Customers forget what was agreed, and there is no simple record of photos, notes and sign off.
- Invoices go out late because they are written up at the end of the week.
- Chasing payment is awkward, and bank transfers are hard to match to jobs.
- Existing software feels built for a larger firm with an office manager.
What success looks like for the customer
The owner sends a quote from the van within ten minutes of leaving the customer, finishes the job with photos and a customer signature on the phone, and has the invoice paid by card or bank payment before driving away, or within a few days through a payment link.
3Evidence to gather before the build
The build should start only when these tests are met. Each one has a pass mark agreed in advance.
| Test | Method | Pass mark |
|---|---|---|
| The problem is real and frequent | 15 interviews with owners of trades businesses with one to ten people | At least 10 describe late invoicing or slow quoting as a weekly problem |
| They will switch | Same interviews, asked what they use now and what it costs | At least 6 pay for or would pay for a tool today |
| Price point | Landing page with two price options and a waiting list | At least 100 sign ups at the higher price in four weeks of paid traffic |
| Payment on the day | Interviews and a clickable prototype | At least 8 of 15 say they would ask customers to pay on site by card or payment link |
| Channel | Test adverts in two trade communities and one trade supplier partnership | Cost per waiting list sign up under £15 |
If the switching test fails, the product is not worth building as a standalone business. If only the price test fails, we would rework the pricing and test again before committing to the build.
4Scope of the first version
In scope
- Sign up for a business, invite up to ten team members, and set roles for owner, office and engineer.
- Customers and sites, with UK address lookup and notes.
- Enquiries captured by hand or from a web form the business can put on its own site.
- Quotes built from a saved price list, with labour, materials and VAT, sent as a branded PDF and a link the customer can accept online.
- A diary showing visits by person, with drag and drop on the web and a day list on the phone.
- Job sheets on the phone with checklists, photos, notes, materials used and a customer signature, working offline.
- Invoices created from the job in one step, sent by email and text, with a card and bank payment link.
- Payment status, automatic reminders and a simple list of who owes what.
- Export of invoices and payments to CSV for the accountant.
Out of scope for the first version
- Accounting software integration, which comes second, starting with the most requested package from the pilot.
- Stock control, purchase orders and supplier price feeds.
- Timesheets, payroll and vehicle tracking.
- Certificates for regulated work, such as gas safety records, which carry their own compliance requirements and should be scoped separately.
- Customer booking portals and marketing tools.
5Main user journeys
| Journey | Starts | Ends | Target time |
|---|---|---|---|
| Quote from site | Owner leaves the customer's house | Customer receives a PDF quote and an accept link | Under 10 minutes on the phone |
| Accept and book | Customer taps accept | Visit appears in the diary and the customer gets a confirmation | Under 1 minute for the customer |
| Do the job | Engineer opens the day list | Job sheet complete with photos and signature, even with no signal | No extra time beyond the work itself |
| Get paid | Job marked complete | Invoice sent and paid by card or bank payment | Paid on site, or reminders run until paid |
| Office overview | Office user opens the web app | Sees the diary, open quotes and unpaid invoices | One screen |
6Architecture overview
The product runs on AWS in the London region (eu-west-2), using serverless services wherever they fit, so running costs stay low while customer numbers are small and rise in line with use. One React codebase produces the web app and the iOS and Android apps: the web build is served through CloudFront, and the same build is packaged as native apps with Capacitor, which gives access to the camera, local storage, push notifications and location. The mobile apps work offline and sync when signal returns. Both share one API and one data model.
A small number of external services do specialist jobs better than we could build them: Stripe for payments, Resend for email, Cloudflare for DNS, bot protection and the marketing site, Matomo for product and marketing analytics, Sentry for error tracking, and Firebase Cloud Messaging for push notifications.
Capacitor 8.5, encrypted SQLite
React 19.3, Vite 8.3
accept quote, pay invoice
Cloudflare Pages
security headers, caching
managed rules, rate limits, Bot Control
passkeys, MFA, threat protection
JWT authoriser
TypeScript, Hono
Serverless v2, row level security
web build, photos, PDFs
PDF, email, SMS, push
reminders
Components
| Component | Responsibility | Technology |
|---|---|---|
| App codebase | Shared screens, state, validation and offline logic for web and mobile | React 19.3, Vite 8.3, React Router, TanStack Query, Zod, Tailwind CSS |
| Mobile apps | Day list, job sheets, quotes, photos, signatures, offline store and sync, push notifications | Capacitor 8.5 with the official Camera, Filesystem, Geolocation, Network, Preferences, Push Notifications and App plugins, and @capacitor-community/sqlite with SQLCipher encryption |
| Web app | Office diary, quotes, invoices, customers, settings and reports | The same React build, served as static files from S3 through CloudFront |
| Customer pages | Quote acceptance and invoice payment without an account | Routes in the same React app using signed links that expire, protected by Cloudflare Turnstile |
| Marketing site | Product pages, pricing, sign up and the waiting list | Static site on Cloudflare Pages, behind Cloudflare WAF, with Turnstile on forms |
| API | Reads and writes for the apps, plus webhooks | API Gateway HTTP API with a Cognito JWT authoriser, Lambda functions in TypeScript using the Hono router |
| Sync service | Receives batches of offline changes and returns changes since the last sync | Lambda endpoint with a change log table in PostgreSQL |
| Document service | Branded quote and invoice PDFs | Worker Lambda rendering PDFs from React templates, stored in S3 |
| Quotes, invoices, reminders, sign in codes and team invitations | Resend, with React Email templates, a sending domain verified in Cloudflare DNS, and webhooks for delivery, bounce and complaint events | |
| Text messages | Visit confirmations and optional reminders | AWS End User Messaging SMS with a registered UK sender ID |
| Push notifications | New and changed visits for engineers | Firebase Cloud Messaging, which delivers to Android directly and to iOS through Apple Push Notification service |
| Payments | Card and bank payments paid straight into each business's own account | Stripe Connect with Express accounts, Checkout and Payment Links, webhooks to the API |
| Scheduling | Quote follow ups and payment reminders | EventBridge Scheduler, one schedule per reminder, cancelled when paid |
| Address lookup | UK address search and map pins | Amazon Location Service places search |
| Analytics | Marketing site visits, sign up funnel, feature use in the apps | Matomo Cloud, cookieless on the marketing site, with consent in the apps |
| Error tracking | Front end and back end errors, crashes, slow screens | Sentry, using @sentry/capacitor and @sentry/react in the apps and the Sentry Lambda layer in the API |
| Data | Business records, change log, audit trail | Aurora PostgreSQL 18.6 Serverless v2, accessed through RDS Proxy |
| Files | Photos, signatures, PDFs | S3 with presigned uploads, served through CloudFront with signed URLs |
| Identity | Sign in for business users | Amazon Cognito user pool with passkeys, emailed codes sent through Resend, optional MFA and threat protection |
7Technology and frameworks
We use current stable releases and pin exact versions in the repository at the start of the build. Where the newest release is not yet supported by a managed service, we use the newest supported release and record the upgrade as a planned task.
| Area | Choice | Version | Notes |
|---|---|---|---|
| Language | TypeScript | 7.0 | Native compiler for builds; TypeScript 6 kept alongside for typescript-eslint until TypeScript 7.1 ships its API |
| Runtime, local and CI | Node.js | 26 LTS | Current long term support line |
| Runtime, Lambda | Node.js | 24 LTS (nodejs24.x) |
Lambda's Node.js 26 runtime is in preview; we move to nodejs26.x once it is generally available |
| User interface | React | 19.3 | One codebase for web and mobile |
| Build tool | Vite | 8.3 | Rolldown bundler |
| Mobile runtime | Capacitor | 8.5.3 | Adopts the iOS scene life cycle needed for Xcode 27; Capacitor 9 is planned as an upgrade once it is released and supported by the plugins we use |
| Offline store | @capacitor-community/sqlite |
Current stable | SQLite with SQLCipher encryption on the device |
| API routing | Hono | Current stable | Small router that runs well on Lambda |
| Validation | Zod | Current stable | Shared schemas between the apps and the API |
| Database | Aurora PostgreSQL | 18.6 | Serverless v2, with scale to zero for development environments |
| Data access | Drizzle ORM and drizzle-kit | Current stable | Typed queries and versioned migrations |
| Infrastructure as code | AWS CDK v2 in TypeScript | Current stable | One app, separate stacks per concern; Cloudflare records managed with the Cloudflare Terraform provider or the Cloudflare API from the same pipeline |
| Resend and React Email | Current | Transactional email only; marketing email is out of scope | |
| Analytics | Matomo Cloud | 5.14 | Matomo 6 is in beta; we stay on the stable line |
| Error tracking | Sentry | Current | Source maps uploaded from CI |
| Mobile builds | Xcode 27, Android Studio and Gradle, fastlane | Current | Signed builds on GitHub Actions macOS and Linux runners, uploaded to TestFlight and the Play internal track |
| Tests | Vitest, Playwright, Maestro | Current stable | Unit and API tests, web end to end tests, mobile flow tests on device builds |
| Source and CI | GitHub and GitHub Actions | Current | Deploys to AWS through OIDC, without stored keys |
8Infrastructure definition
AWS accounts and regions
All infrastructure is defined in code and deployed through the pipeline. Nothing is created by hand in the console, except the initial organisation set up.
| Account | Purpose |
|---|---|
| Management | AWS Organizations, billing, IAM Identity Center, service control policies |
| Security | Delegated administrator for GuardDuty, Security Hub, Inspector, Macie and IAM Access Analyzer |
| Log archive | Central, locked S3 bucket for CloudTrail, AWS Config, WAF and CloudFront logs |
| Development | Shared development environment, with scale to zero database |
| Staging | Copy of production with synthetic data, used for release testing and pilot demos |
| Production | Live service, with access through IAM Identity Center only |
Primary region is Europe (London), eu-west-2. CloudFront certificates and the CloudFront WAF web ACL sit in us-east-1 because AWS requires it. Backups are copied to Europe (Ireland), eu-west-1. Service control policies block other regions, stop CloudTrail being turned off, and stop public S3 buckets being created.
Cloudflare
Cloudflare holds the domain and the authoritative DNS, with DNSSEC switched on. Records for the app, the API and file downloads point to CloudFront with the Cloudflare proxy off, so AWS WAF and CloudFront are the only layer in front of the application and requests are not inspected or cached twice. The marketing site runs on Cloudflare Pages with the proxy on, behind Cloudflare WAF managed rules and rate limiting. Turnstile protects the sign up form, the waiting list, the public enquiry form and the customer quote and invoice pages, and the API verifies each Turnstile token before acting on the request. Resend's SPF, DKIM and DMARC records, and the Matomo tracking domain, are kept in the same Cloudflare zone, all managed from the pipeline.
Network
| Item | Definition |
|---|---|
| VPC | One per environment, two Availability Zones, private subnets for Lambda and the database, no public database access |
| Egress | One NAT gateway in development and staging, two in production; VPC endpoints for S3, Secrets Manager, SQS, EventBridge and KMS to reduce NAT traffic; outbound calls to Stripe, Resend, Sentry and Firebase only |
| CloudFront | Separate distributions for the web app, the API and files; TLS 1.2 or later; origin access control so S3 buckets are reachable only through CloudFront; a response headers policy that sets a strict Content Security Policy, HSTS with preload, X-Content-Type-Options, Referrer-Policy and Permissions-Policy |
| AWS WAF | Web ACL on each CloudFront distribution with the AWS managed core rule set, known bad inputs, IP reputation and anonymous IP lists, SQL injection rules, Bot Control in common mode, and rate rules on sign in, sync, uploads, public links and webhooks; logs sent to the log archive account |
| Shield | Shield Standard on the CloudFront distributions, which AWS includes at no extra cost |
| API Gateway | Reachable only through CloudFront, enforced with a secret header checked by WAF and a resource policy; throttling per route |
Stacks
| Stack | Resources |
|---|---|
network |
VPC, subnets, NAT, VPC endpoints, security groups |
data |
Aurora PostgreSQL Serverless v2 cluster (writer and one reader in production), RDS Proxy, KMS key, Secrets Manager secret with rotation, AWS Backup plan with cross region copy |
storage |
S3 buckets for the web build, uploads and documents, with block public access, KMS encryption, versioning, object lock on documents, lifecycle rules and origin access control |
identity |
Cognito user pool with passkeys and threat protection, app clients for mobile and web, groups for owner, office and engineer, pre token generation trigger that adds the tenant ID, custom email sender trigger that sends codes through Resend |
api |
HTTP API, JWT authoriser, Lambda functions, log groups with 90 day retention, provisioned concurrency off by default |
async |
EventBridge bus, SQS queues with dead letter queues, worker Lambdas, EventBridge Scheduler group and role |
messaging |
Secrets for Resend, Firebase and Stripe; End User Messaging SMS sender ID; webhook routes for Resend and Stripe events |
edge |
CloudFront distributions, response headers policies, AWS WAF web ACLs in us-east-1, ACM certificates |
security |
GuardDuty with S3, Lambda and RDS protection; Security Hub with the AWS Foundational Security Best Practices and CIS standards; Inspector for Lambda code and dependencies; Macie on the uploads bucket; AWS Config rules; IAM Access Analyzer |
observability |
CloudWatch dashboards, alarms, CloudWatch Synthetics canaries, SNS topic to the support rota, X-Ray groups, AWS Budgets and Cost Anomaly Detection |
cloudflare |
DNS records, DNSSEC, Pages project, WAF and rate limiting rules for the marketing site, Turnstile widgets (Terraform, run from the same pipeline) |
Example CDK structure
// infra/bin/app.ts
const app = new cdk.App();
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'eu-west-2' };
const edgeEnv = { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' };
const stage = app.node.tryGetContext('stage') as 'dev' | 'staging' | 'prod';
const network = new NetworkStack(app, `${stage}-network`, { env, stage });
const data = new DataStack(app, `${stage}-data`, { env, stage, vpc: network.vpc });
const storage = new StorageStack(app, `${stage}-storage`, { env, stage });
const identity = new IdentityStack(app, `${stage}-identity`, { env, stage });
const asyncStack = new AsyncStack(app, `${stage}-async`, { env, stage, vpc: network.vpc, data, storage });
const api = new ApiStack(app, `${stage}-api`, { env, stage, vpc: network.vpc, data, storage, identity, bus: asyncStack.bus });
new MessagingStack(app, `${stage}-messaging`, { env, stage });
new EdgeStack(app, `${stage}-edge`, { env: edgeEnv, crossRegionReferences: true, stage, storage, api });
new SecurityStack(app, `${stage}-security`, { env, stage });
new ObservabilityStack(app, `${stage}-observability`, { env, stage, api, data, asyncStack });
// infra/lib/data-stack.ts (extract)
const cluster = new rds.DatabaseCluster(this, 'Db', {
engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.of('18.6', '18') }),
writer: rds.ClusterInstance.serverlessV2('writer'),
readers: props.stage === 'prod' ? [rds.ClusterInstance.serverlessV2('reader', { scaleWithWriter: true })] : [],
serverlessV2MinCapacity: props.stage === 'dev' ? 0 : 0.5,
serverlessV2MaxCapacity: props.stage === 'prod' ? 8 : 2,
vpc: props.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
storageEncrypted: true,
storageEncryptionKey: dbKey,
backup: { retention: cdk.Duration.days(props.stage === 'prod' ? 35 : 7) },
deletionProtection: props.stage === 'prod',
cloudwatchLogsExports: ['postgresql'],
});
const proxy = cluster.addProxy('Proxy', { secrets: [cluster.secret!], vpc: props.vpc, requireTLS: true, iamAuth: true });
// infra/lib/edge-stack.ts (extract)
const acl = new wafv2.CfnWebACL(this, 'AppAcl', {
scope: 'CLOUDFRONT',
defaultAction: { allow: {} },
visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: `${props.stage}-app-acl`, sampledRequestsEnabled: true },
rules: [
managedRule('AWSManagedRulesCommonRuleSet', 10),
managedRule('AWSManagedRulesKnownBadInputsRuleSet', 20),
managedRule('AWSManagedRulesAmazonIpReputationList', 30),
managedRule('AWSManagedRulesAnonymousIpList', 40),
managedRule('AWSManagedRulesSQLiRuleSet', 50),
managedRule('AWSManagedRulesBotControlRuleSet', 60),
rateRule('SignInRate', '/v1/auth', 100, 70),
rateRule('SyncRate', '/v1/sync', 600, 80),
rateRule('PublicLinkRate', '/v1/public', 120, 90),
],
});
Environments and releases
| Environment | Deployed when | Data |
|---|---|---|
| Development | Each merge to the main branch | Seed data |
| Staging | Each release candidate | Synthetic data that matches production volumes |
| Production | After staging checks pass and a named person approves | Live data |
Database migrations run as a separate pipeline step before the new Lambda code is switched on, and each migration must work with both the old and new code so a release can be rolled back.
9Data model
All business data carries a tenant_id. PostgreSQL row level security policies restrict each query to the tenant in the caller's token, so a fault in application code cannot return another business's records. Identifiers are UUIDv7, generated on the device, so records created offline keep the same ID once synced.
| Table | Purpose | Main fields |
|---|---|---|
tenant |
A trades business | name, trading address, VAT number, logo, Stripe account ID, plan, status |
app_user |
A person who signs in | Cognito subject, name, email, phone |
membership |
Links users to a business | tenant, user, role (owner, office, engineer), status |
customer |
A customer of the business | name, email, phone, marketing consent, notes |
site |
A property where work happens | customer, address lines, postcode, location point, access notes |
enquiry |
A request for work | customer, site, source, description, status |
price_item |
Saved labour and materials | name, unit, unit price, VAT rate, active |
quote |
A priced offer | enquiry, number, status (draft, sent, viewed, accepted, declined, expired), valid until, totals |
quote_line |
A line on a quote | quote, price item, description, quantity, unit price, VAT rate |
job |
Accepted work | quote, customer, site, status, assigned users |
visit |
A booked appointment | job, user, start, end, status |
job_sheet |
Work recorded on site | visit, checklist results, notes, materials used, signature file, completed at |
attachment |
Photos and files | owner record, S3 key, content type, size, taken at |
invoice |
A request for payment | job, number, status (draft, sent, part paid, paid, overdue, void), due date, totals |
invoice_line |
A line on an invoice | invoice, description, quantity, unit price, VAT rate |
payment |
Money received | invoice, method (card, bank, cash, transfer), amount, Stripe payment ID, received at |
reminder |
Scheduled follow ups | target record, schedule name, send at, status |
change_log |
Feed for offline sync | tenant, table, record ID, version, changed fields, changed at |
audit_event |
Who did what | tenant, user, action, record, before and after, at |
Money is stored as integer pence with an ISO currency code. VAT is calculated per line and rounded per line, matching the way most UK invoices are presented, and the rounding rule is covered by tests.
10API design
The API is a JSON API over HTTPS, versioned in the path. Requests from the apps carry a Cognito access token. Public customer links use signed tokens that expire and identify one quote or invoice only.
| Method and path | Purpose |
|---|---|
POST /v1/tenants |
Create a business after sign up |
GET /v1/me |
Current user, memberships and roles |
GET, POST /v1/customers and GET, PATCH /v1/customers/{id} |
Customers |
GET /v1/addresses/search?q= |
UK address search |
GET, POST /v1/quotes and PATCH /v1/quotes/{id} |
Quotes |
POST /v1/quotes/{id}/send |
Render the PDF and send by email and text |
GET /v1/public/quotes/{token} and POST /v1/public/quotes/{token}/accept |
Customer views and accepts a quote |
GET, POST /v1/visits |
Diary |
POST /v1/jobs/{id}/complete |
Complete a job and optionally create the invoice |
POST /v1/invoices/{id}/send |
Send an invoice with a payment link |
GET /v1/public/invoices/{token} |
Customer views an invoice and pays |
POST /v1/uploads |
Get a presigned S3 upload URL |
POST /v1/sync/push and GET /v1/sync/pull?since= |
Offline sync |
POST /v1/webhooks/stripe |
Stripe payment and account events, verified by signature |
GET /v1/reports/unpaid |
Unpaid invoices by age |
Errors follow RFC 9457 problem details. Writes accept an idempotency key so a request retried after a dropped connection does not create a duplicate quote, invoice or payment.
11Detailed design
Offline working and sync
The mobile apps keep an encrypted SQLite copy of the user's upcoming visits, their jobs, customers and sites, the price list and open quotes. The encryption key is generated on first sign in and held in the iOS Keychain or Android Keystore. Every change on the device is written locally first and added to an outbox. When the Capacitor Network plugin reports a connection, the app pushes the outbox in batches and then pulls changes from the server since its last sync point. The web app works online only in the first version.
The server applies each change with a version check. If the record has changed on the server since the device last saw it, field level rules decide the outcome: notes and checklist answers are merged, the job status follows a fixed order so a completed job is not reopened by a stale device, and price fields keep the server's version and flag the conflict to the office. Photos are compressed on the device, written to app storage with the Filesystem plugin and uploaded separately in the background, resuming after a dropped connection, so a job sheet can sync before its photos have finished.
Mobile app shell
The Capacitor apps load the React build from the app bundle, not from a remote URL, so the app opens and works without signal. Native features are reached only through the Capacitor plugins listed in section 6, wrapped in one module so the web build falls back cleanly where a feature does not exist in the browser. The apps request camera, location and notification permissions at the point of use, with a plain explanation. Updates to the web layer ship through the app stores in the first version; over the air updates can be added later within Apple and Google rules.
Quotes and invoices
Quote and invoice numbers are generated per business from a database sequence so they are gap free and in order, which accountants expect. PDFs are produced by a worker from React templates, stored in S3 with the document version, and regenerated only when the document changes. Once an invoice is sent, its lines are locked; changes are made by issuing a credit note and a new invoice, so the record matches what the customer received.
Payments
Each business connects its own Stripe account through Stripe Connect onboarding, so customer payments go straight to the business and we do not hold client money. The invoice link opens a Stripe Checkout page offering card, Apple Pay, Google Pay and bank payment where Stripe supports it for the account. Payment status is updated only from Stripe webhooks whose signatures are verified, processed once using the event ID. Cash and bank transfers can be recorded by hand by the owner or office user.
Email through Resend
Workers send quotes, invoices, reminders, invitations and sign in codes through the Resend API, using React Email templates branded with the business's name and logo. Mail is sent from a subdomain dedicated to transactional email, with SPF, DKIM and a DMARC policy published in Cloudflare DNS, and the reply address set to the business's own email. Each send carries an idempotency key so a retried worker does not send twice. Resend webhooks, verified by signature, record delivery, bounce and complaint events against the customer, so the office can see when an email address is wrong, and repeated bounces stop further sends to that address. Cognito sign in codes go through a custom email sender trigger that decrypts the code with KMS and sends it through Resend, so all email comes from one service with one set of records.
Reminders
When a quote or invoice is sent, the API creates EventBridge Scheduler schedules for the follow ups the business has chosen, for example three, seven and fourteen days. Each schedule puts a message on a queue, and the worker checks the record is still unpaid or unaccepted before sending anything. Paying or accepting cancels the remaining schedules.
Push notifications and texts
The Capacitor Push Notifications plugin registers each device with Firebase Cloud Messaging, and the API stores the device token against the user and the device. Workers send notifications for new or changed visits through the FCM HTTP v1 API, which delivers to Android directly and to iOS through Apple Push Notification service. Notifications carry no customer details, only a prompt to open the app. Texts to customers go through End User Messaging with the business name as the sender ID.
Identity and roles
Business users sign in with passkeys or an emailed code, and owners can require MFA for their team. Cognito threat protection blocks sign in attempts that use known leaked credentials and challenges unusual sign in patterns. Tokens are kept in secure storage on the device and refreshed silently. Roles are held in the database and copied into the token at sign in. Engineers see their own visits and the jobs assigned to them; office users see everything except billing settings; owners see everything. Removing a team member revokes their sessions and their device copy is cleared at the next sync.
Analytics with Matomo
Matomo Cloud, hosted in the EU, measures the marketing site, the sign up funnel and how the apps are used. The marketing site runs Matomo without cookies, so no consent banner is needed there. The web and mobile apps ask for consent before tracking, record events by feature such as quote sent or invoice paid rather than by customer, and do not send customer names, addresses or amounts. Goals in Matomo match the launch measures in section 17, so the pilot dashboard and the product analytics agree.
Search and lists
Lists in the web app are paged on the server, with indexes on tenant, status and date. Customer search uses PostgreSQL trigram indexes on name, email, phone and postcode, which is enough for businesses at this size without a separate search service.
12Security and privacy
| Area | Design |
|---|---|
| Data protection | The business is the controller of its customers' data and we act as processor under a data processing agreement; data stays in the UK region, with backups in Ireland; Stripe, Resend, Sentry, Matomo, Cloudflare and Firebase are listed as sub processors with their own agreements |
| Edge protection | AWS WAF managed rules, Bot Control and rate limits on each CloudFront distribution; Shield Standard; Cloudflare WAF and Turnstile on the marketing site and public forms; API Gateway reachable only through CloudFront |
| Browser and app hardening | Strict Content Security Policy, HSTS with preload and other security headers from CloudFront; the Capacitor apps load code from the app bundle only, with navigation limited to our own domains and Stripe Checkout |
| Encryption | TLS 1.2 or later in transit; KMS encryption for the database, S3 and backups with separate keys per environment; SQLCipher encryption for the offline store, with the key in the Keychain or Keystore |
| Tenant isolation | Row level security in PostgreSQL, tenant checked in the API, and tests in each build that try to read across tenants |
| Identity | Passkeys, MFA for owners, Cognito threat protection, short lived access tokens, refresh tokens in secure device storage |
| Secrets | Secrets Manager with rotation for the database, Stripe, Resend and Firebase keys; no secrets in code or in mobile builds |
| Staff access | IAM Identity Center with MFA; production access through approved roles only, recorded in CloudTrail; service control policies across the organisation |
| Threat detection | GuardDuty with S3, Lambda and RDS protection; Security Hub with the AWS Foundational Security Best Practices and CIS standards; Inspector scanning Lambda code and dependencies; Macie checking the uploads bucket for unexpected personal data; IAM Access Analyzer for external access |
| Application security | Dependency, secret and static analysis scans in CI; OWASP ASVS level 2 and OWASP MASVS checks before launch; an external penetration test before public launch |
| Threat model | Written at the start of the build and reviewed before launch, covering public links, webhooks, file uploads, sync, lost devices and account takeover |
| Audit | Audit trail of user actions in the database; CloudTrail, AWS Config, WAF and CloudFront logs kept in the locked log archive account |
| Retention | Customer data kept while the business is a subscriber and for a set period after, then deleted; invoices kept for six years by default to support VAT record keeping |
| Rights requests | Tools for the business to export or delete a customer's data |
13Quality, testing and releases
Each change goes through a pull request with automated checks: type checks, lint, unit tests, API tests against a disposable PostgreSQL database, a CDK synth and diff, a Terraform plan for Cloudflare, and security scans. Web end to end tests run with Playwright against staging. Mobile flows, including working offline and syncing afterwards, run with Maestro on iOS and Android builds produced by the pipeline.
Releases to production need passing checks and a named approver. The web app is released by uploading the new build to S3 and invalidating CloudFront. Mobile releases are built and signed in CI with fastlane, go to TestFlight and the Play internal track first, and are then released to the stores in stages. Sentry receives the source maps and release version for each build, so errors can be traced to the exact release.
14Monitoring and support
Each request carries a trace ID from the app through the API to the database and the workers, using AWS X-Ray through OpenTelemetry. Monitoring is split by what each tool does best.
| Tool | What it watches |
|---|---|
| CloudWatch metrics and dashboards | API latency and errors by route, Lambda errors and throttles, database capacity and connections, queue depth, dead letter messages, scheduler failures |
| CloudWatch alarms | Error rates, latency, dead letter messages, failed Stripe or Resend webhooks, database capacity close to its limit, backup failures |
| CloudWatch Synthetics | Canaries every five minutes that sign in, load the diary, open a public quote link and call the health endpoint |
| CloudWatch Logs Insights | Structured JSON logs with tenant and request IDs, and no customer personal data |
| Sentry | Errors and crashes in the web and mobile apps and the API, slow screens, release health and crash free sessions |
| Resend dashboard and webhooks | Delivery, bounce and complaint rates, with an alarm if bounces rise above a set rate |
| Stripe dashboard and webhooks | Failed payments, disputes and Connect account issues |
| AWS WAF and Cloudflare analytics | Blocked requests, bot traffic and rate limit hits |
| Security Hub and GuardDuty | Security findings, sent to the support rota by severity |
| Matomo | Funnel and feature use, matched to the launch measures |
| AWS Budgets and Cost Anomaly Detection | Monthly spend against budget and unusual cost spikes |
Alarms go to the support rota through SNS, with a runbook linked from each alarm. Service levels for the first version are 99.5% monthly availability for the API and web app, with a recovery point objective of 5 minutes and a recovery time objective of 4 hours, both tested before launch by restoring a backup into staging.
15Running cost estimate
These are our estimates, before Stripe fees, text message charges and app store fees. They assume modest use at each stage and should be confirmed at the start of the build with the AWS Pricing Calculator and the current plans of each external service, because London prices differ from the US list prices AWS uses in its examples and external plans change.
| Stage | Assumption | Estimated AWS cost per month | Estimated external services per month |
|---|---|---|---|
| Build | Development and staging, scale to zero database in development | £150 to £300 | Free or entry plans for Resend, Sentry and Cloudflare; Matomo Cloud entry plan |
| Pilot | 10 to 20 businesses, around 2,000 jobs a month | £350 to £600 | £50 to £150 |
| Early growth | 500 businesses, around 50,000 jobs a month | £1,000 to £1,800 | £200 to £500 |
The largest fixed AWS items are NAT gateways, RDS Proxy, AWS WAF with Bot Control, and the minimum database capacity in production. Texts cost more per message than emails, so text reminders are off by default and switched on per business.
16Build plan
The build follows our standard three month plan. A senior architect leads the build and makes the design decisions, AI agents do the heavy lifting on code, tests and documentation, and a product lead runs the pilot recruitment and customer testing.
| Weeks | Focus | Output |
|---|---|---|
| 1 to 2 | Evidence tests, design and foundations | Interview findings, clickable prototype, threat model, AWS accounts, CI pipeline, CDK stacks for network, data, storage and identity, Cloudflare zone and DNS, Resend sending domain, Sentry and Matomo projects |
| 3 to 4 | Customers, sites, price list and quotes | Web screens and the Capacitor app shell on iOS and Android, quote PDF, customer accept page with Turnstile, Resend email templates |
| 5 to 6 | Diary, visits and job sheets | Web diary, mobile day list, encrypted offline store, job sheets with photos and signatures, push notifications through Firebase |
| 7 to 8 | Sync and invoicing | Offline sync with conflict rules, invoices, credit notes, numbering |
| 9 | Payments and reminders | Stripe Connect onboarding, payment links, webhooks, scheduled reminders |
| 10 | Hardening | Load test, AWS WAF and Cloudflare rule tuning, Security Hub findings cleared, external penetration test, restore test, synthetic canaries and alarms live, accessibility checks to WCAG 2.2 AA on the web app |
| 11 | Pilot launch | Ten pilot businesses onboarded, TestFlight and Play internal builds, support rota live |
| 12 | Pilot results and handover | Pilot metrics, store submission, documentation pack, handover sessions with the receiving team |
Milestones and stop criteria
| Milestone | Week | Continue if | Stop or rethink if |
|---|---|---|---|
| Evidence tests | 2 | Pass marks in section 3 are met | Switching test fails |
| Quote journey | 4 | Five interviewees send a test quote in under ten minutes | Most need help to finish a quote |
| Offline job sheet | 6 | Job sheet completes with no signal and syncs cleanly | Sync conflicts lose data in testing |
| Pilot | 12 | At least six of ten pilot businesses send three or more invoices in a fortnight | Fewer than three businesses use it weekly |
17Launch plan
The pilot runs with ten businesses recruited from the interviews and waiting list, free for the first two months in return for weekly feedback. Public launch follows when the pilot measures below are met for four weeks in a row.
| Measure | Target |
|---|---|
| Activation | Business sends its first invoice within 7 days of sign up |
| Time to quote | Median under 10 minutes from creating a quote to sending it |
| Paid through the app | At least 40% of invoices paid by link or on site |
| Days to payment | Median under 7 days from invoice to payment |
| Weekly use | At least 70% of businesses active each week |
| Pilot to paid | At least 6 of 10 pilot businesses agree to pay at the end of the free period |
Pricing to test at launch is a monthly subscription per business, with one price for a sole trader and one for a team of up to ten, plus card fees passed through at cost. The channels to test first are trade communities online, a trade supplier partnership with counter promotion, and referrals from accountants who serve trades businesses.
18Risks
| Risk | Effect | Response |
|---|---|---|
| Strong competitors already in the market | Hard to win customers on features | Compete on simplicity and getting paid on the day, and test this in the evidence stage before building |
| Sync conflicts | Lost or wrong job data damages trust | Field level conflict rules, test cases for each conflict, and an office view of flagged conflicts |
| Low adoption of payment links | Weakens the main reason to switch | Test in interviews; make cash and transfer recording quick so the product still works without them |
| App store review delays | Pilot start slips | Submit TestFlight and Play internal builds by week 9 |
| Capacitor 9 release during the build | Plugin updates and iOS changes mid build | Build on Capacitor 8.5, which already supports Xcode 27, and plan the Capacitor 9 upgrade after launch once the plugins we use support it |
| Email landing in spam | Quotes and invoices not seen | Dedicated sending subdomain, SPF, DKIM and DMARC from week 1, bounce and complaint alarms, and the business's own address as the reply address |
| Dependence on external services | An outage at Stripe, Resend or Firebase affects one feature | Queue and retry through SQS, show the status in the app, and keep each provider behind one module so it can be replaced |
| Text message costs | Running costs rise with use | Texts off by default, per business limits, email first |
| Regulated work creeping into scope | Compliance burden in the first version | Keep certificates out of the first version and scope them separately |
19Decisions for you
- Confirm the first customer group, including whether to start with one trade or several.
- Choose the product name, so domains, app store listings and the sender ID can be registered in week 1.
- Agree the pilot terms, including the free period and what pilot businesses commit to.
- Choose who takes the product on after the build: your own senior technology team or a development partner you trust, so the handover pack is written for them.
20What the build hands over
At the end of the build, the receiving team gets the code in your own repository, the AWS accounts in your own organisation, the Cloudflare, Resend, Stripe, Matomo, Sentry, Firebase and app store accounts in your name, and a documentation pack covering the architecture and decision records, the data model, the API reference, the CDK stacks and Cloudflare configuration, the monitoring dashboards and alarms, runbooks for releases, incidents, backups and restores, the threat model and security test results, the cost model, the pilot results and the open backlog. We walk the receiving team through it, they make a release themselves with us alongside, and the build is accepted when they confirm they can run, change and support the product.
* All prices are subject to VAT.