Handypro
A home-services marketplace for your city: a customer app that books cleaners, plumbers, electricians and other pros, a Handypro Pro app for the pros and their teams, and one Next.js server that is the website, the pro web panel, the admin panel and the API. You add your own keys; everything else is ready.
1. Welcome
Thank you for buying Handypro. This guide is written for beginners: follow the chapters in order and you will have the server, the website, the admin panel and both apps running under your own name and keys.
What is in the download
| Folder | What it is |
|---|---|
handypro_customer_flutter/ | The customer app (iOS and Android). People pick an address, browse services and pros, book a time or ask for a pro right now, pay by card, wallet or cash, track the pro on a map, chat, tip, review, post custom jobs for bids, and open warranty claims and disputes. |
handypro_provider_flutter/ | The Handypro Pro app for pros and companies: sign-up with documents, go online, accept requests, run the job (on my way, start code, before/after photos, extra charges, finish), calendar and time off, services and prices, staff, job leads and bids, promo codes, plans, earnings and withdrawals, reviews and chats. |
handypro_flutter_core/ | Code both apps share: API client, sign-in, theme, widgets and shared pages. Both apps use it through a path dependency, so there is nothing to publish. |
handypro_web_nextjs/ | One Next.js app that is the website (with booking and checkout), the pro web panel (/pro), the admin panel (/admin), the API the apps use (/api/v1) and the background worker (pnpm worker). |
deploy/ | The Docker stack: website/API + worker + PostgreSQL + MinIO file storage, started with one command, with optional automatic HTTPS. |
tools/rename.mjs | Renames both apps, their package ids and the brand name in one command. |
docs/ | This documentation (HTML and PDF). |



How the parts fit
Both apps and the website talk to the same server. All data (services, pros, bookings, payments, chats, reviews) lives in your PostgreSQL database. Photos, job proof photos and pro documents live in S3-compatible storage (MinIO in the Docker stack, or Amazon S3 / Cloudflare R2). Firebase is used only for sign-in and push notifications.
The life of a booking: a customer books a service for a time slot (or asks for an instant pro) → the pro gets a request with a countdown and accepts it (a company assigns a team member) → on the day the pro taps On my way and the customer sees them on the map → the customer reads a 4-digit start code to the pro → the pro takes before photos, works, adds any extra charge the customer approves, and finishes with after photos → the card is charged (or the pro collects cash) → the customer rates and tips → the pro's earnings are paid out on your payout days.
2. Requirements
To run the server
- A Linux server (VPS) with at least 2 CPU cores, 4 GB RAM and 30 GB disk. Ubuntu 24.04 is used in this guide.
- Docker with the Compose plugin (install guide).
- A domain name, for example
your-domain.com, with a DNS A record pointing at the server.
To build the apps
- Flutter 3.44 or newer (Dart 3.12) (install), Android Studio for Android, a Mac with Xcode 26 for iOS (iOS 15 or newer on the phone).
- Node.js 22 or newer for the rename tool, and pnpm 11 (
npm i -g pnpm) if you run the website without Docker.
Accounts you will create (all have free tiers)
- Firebase (required: sign-in and push).
- Optional, when you want them: Stripe (card payments and pro plans on the web panel), RevenueCat (pro plans bought in the Pro app), Twilio (booking SMS), Google Cloud (address search with the Geocoding API), a map tile provider such as MapTiler, and any SMTP email provider.
3. Quick start (Docker)
This gets the whole server running on your VPS in about 15 minutes. You need the Firebase keys from chapter 4 for sign-in; you can start this chapter first and add them before the install wizard.
- Copy the kit to the server, for example with
scp handypro-1.0.0.zip root@YOUR_SERVER_IP:, then on the server:apt install -y unzip unzip handypro-1.0.0.zip cd handypro/deploy cp .env.example .env nano .env - In
.env, fill at least these values:Key What to put POSTGRES_PASSWORDA long random password, letters and digits only (run openssl rand -hex 24to make one).S3_SECRET_ACCESS_KEYAnother random value for the built-in MinIO storage (at least 8 characters). APP_URLYour website address, e.g. https://your-domain.com(orhttp://YOUR_SERVER_IP:3000for a first test).NEXT_PUBLIC_FIREBASE_*,FIREBASE_*From chapter 4. - Start everything:
The first build takes 5–10 minutes. On every start the database tables are created or updated, and the base data (categories, plans, a first zone and tax rule) is added if it is missing.docker compose up -d --build - Open
APP_URL/installin your browser. The install wizard creates your admin account, names the business, sets the currency and colour, and can load the sample marketplace (chapter 7). - Check it:
APP_URL/api/v1/healthshows{"ok":true,…}. Then put it on your domain with HTTPS (chapter 5).
.env? Run docker compose up -d again. The Firebase web values are read when the page loads, so no rebuild is needed for them.4. Firebase setup
Firebase handles sign-in (phone, Google, Apple and guest in the apps; email and password for admin staff) and push notifications. Your data stays in your own database. One Firebase project serves the website and both apps.
- Go to the Firebase console → Add project. Give it your business name.
- Build → Authentication → Get started → Sign-in method. Turn on: Phone, Google, Apple (see chapter 16), Anonymous (customers browsing as guests) and Email/Password (admin staff).
- Phone sign-in: Authentication → Settings → SMS region policy — allow the countries your users are in. Without it Firebase refuses to send the code.
- Authentication → Settings → Authorized domains: add
your-domain.com. - Project settings (gear) → General → Your apps → Add app → Web (name it "Website"). Copy the values from the
firebaseConfigshown intodeploy/.env:NEXT_PUBLIC_FIREBASE_API_KEY="…apiKey…" NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your-project.firebaseapp.com" NEXT_PUBLIC_FIREBASE_PROJECT_ID="your-project" NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="your-project.firebasestorage.app" NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="…" NEXT_PUBLIC_FIREBASE_APP_ID="1:…:web:…" - Project settings → Service accounts → Generate new private key. A JSON file downloads. Copy three values from it into
deploy/.env:
Keep theFIREBASE_PROJECT_ID="your-project" FIREBASE_CLIENT_EMAIL="firebase-adminsdk-xxxx@your-project.iam.gserviceaccount.com" FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIE…\n-----END PRIVATE KEY-----\n"\nas they are in the file. Keep this file secret. - Register the four mobile apps: Project settings → Your apps → Add app → Android and iOS for the customer app (e.g.
com.yourcompany.handypro) and Android and iOS for the Pro app (e.g.com.yourcompany.handypro.provider). Rename the apps first if you want your own ids (chapter 17). Or let the FlutterFire CLI register them:dart pub global activate flutterfire_cli cd handypro_customer_flutter flutterfire configure --project your-project --platforms android,ios \ --android-package-name com.yourcompany.handypro --ios-bundle-id com.yourcompany.handypro cd ../handypro_provider_flutter flutterfire configure --project your-project --platforms android,ios \ --android-package-name com.yourcompany.handypro.provider --ios-bundle-id com.yourcompany.handypro.provider - Copy each app's values into its own
.env(handypro_customer_flutter/.envandhandypro_provider_flutter/.env):FIREBASE_PROJECT_ID,FIREBASE_MESSAGING_SENDER_ID,FIREBASE_STORAGE_BUCKET,FIREBASE_ANDROID_API_KEY,FIREBASE_ANDROID_APP_ID,FIREBASE_IOS_API_KEY,FIREBASE_IOS_APP_ID,FIREBASE_IOS_CLIENT_ID,FIREBASE_IOS_BUNDLE_ID. You find them in each app'sgoogle-services.json/GoogleService-Info.plist(download them from Project settings). The apps read these values from.env, so you do not need to put the downloaded files in the projects. - Google sign-in on Android needs
GOOGLE_SERVER_CLIENT_IDin both apps'.env: Authentication → Sign-in method → Google → Web SDK configuration → Web client ID. - iOS (Google and phone sign-in): in each app, copy
ios/Flutter/Firebase.xcconfig.exampletoios/Flutter/Firebase.xcconfigand fill inGOOGLE_REVERSED_CLIENT_ID(theREVERSED_CLIENT_IDin that app'sGoogleService-Info.plist) andFIREBASE_ENCODED_APP_ID(itsGOOGLE_APP_IDwith:replaced by-andapp-in front, as the example shows). - Android Google and phone sign-in need your signing key fingerprints: run
cd android && ./gradlew signingReportin each app and add the SHA-1 and SHA-256 under Project settings → Your apps → Android → Add fingerprint. Add the fingerprints of your upload key and of Google Play's app signing key too when you publish.
5. Put it online (VPS, HTTPS)
The quickest way: the stack has a built-in Caddy web server that gets a free HTTPS certificate for your domain.
- Point your domain's DNS A record at the server and wait until it resolves.
- In
deploy/.envsetDOMAIN=your-domain.comandAPP_URL=https://your-domain.com. - Open ports 80 and 443 in the firewall (
ufw allow 80,443/tcp) and start the stack with the https profile:docker compose --profile https up -d - Add
your-domain.comto Firebase → Authentication → Settings → Authorized domains, if you have not yet.
Already run nginx, Traefik or another proxy? Skip the profile and forward your domain to 127.0.0.1:3000 (the WEB_PORT).
Backups
Back up the database every day, for example with a cron job:
docker compose exec -T postgres pg_dump -U handypro handypro | gzip > /root/backups/handypro-$(date +%F).sql.gz
Also back up the minio-data volume (photos and pro documents), or use a cloud bucket (next chapter).
6. Other hosting
Website on Vercel, database on a managed PostgreSQL
- Create a PostgreSQL database (Neon, Supabase, Railway…) and copy its connection string.
- Import
handypro_web_nextjs/into Vercel. Add every key fromhandypro_web_nextjs/.env.exampleas an environment variable, withDATABASE_URLset to your database and the fiveS3_*keys set to a cloud bucket (Vercel has no disk for uploads). - On your computer, create the tables once:
cd handypro_web_nextjs && pnpm install && pnpm prisma:migrate:deploy(withDATABASE_URLin.env). Then open/installon your Vercel domain. - The background worker does not run on Vercel. Run
pnpm workeron any small server or service that keeps a process running (Railway, Render, a VPS) with the same environment variables. Without the worker, unanswered requests do not expire, cards are not authorised before jobs, job requests do not close, plans do not renew and scheduled campaigns stay unsent.
Cloud storage instead of MinIO
Create a private bucket on Amazon S3 or Cloudflare R2 and set S3_ENDPOINT (empty for AWS, your R2 endpoint for R2), S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY. Files are served through the API, so the bucket stays private. Pro ID and insurance documents are only reachable through links that expire after an hour (FILES_SECRET signs them).
Without Docker
Install Node.js 22 and pnpm 11, then in handypro_web_nextjs/: cp .env.example .env, fill it in, and run pnpm install, pnpm prisma:migrate:deploy, pnpm build, then keep pnpm start and pnpm worker running (for example with pm2). Without S3 keys, files are stored in the local uploads/ folder. Open /install to finish.
7. Install wizard and sample data
The first time, open /install on your website. It asks for:
- Your admin account (name, email and password). This is the super admin; you add more staff later in Admin → Roles.
- Your business: app name, currency, distance unit (miles or kilometres) and time zone. Slots, opening hours and every time in the apps, website and panels follow this time zone, wherever the phone or browser is.
- The look and the content: your brand colour, and Load the sample pros and bookings — 18 pros with 58 services in 12 categories, customers, reviews and a year of bookings around Austin, Texas, so every screen has content. Leave it off to start with only the basics (categories, plans, a first zone and tax rule).
The wizard closes for good once a super admin exists. Sign in later at /admin.
8. Setup check
Two ways to see which keys are missing and whether each service answers:
- Admin → Setup check: every key from every
.env.example(server, Docker, both apps) marked set or missing, and a live test of the database, Firebase, push, card payments, plan purchases, storage, email, SMS, address search and the worker. Values are never shown. - Command line: in
handypro_web_nextjs/runpnpm run doctor(with Docker:docker compose exec worker pnpm run doctor).
9. Run the two apps
Do this for handypro_customer_flutter and for handypro_provider_flutter:
- Copy the settings file and fill it in:
Setcd handypro_customer_flutter cp .env.example .envAPI_BASE_URLto your server (https://your-domain.com; for a server on your computer usehttp://10.0.2.2:3000in the Android emulator) and the Firebase values from chapter 4. The Pro app also takesREVENUECAT_ANDROID_KEYandREVENUECAT_IOS_KEY(chapter 12). - Get the packages and run:
flutter pub get flutter run
Until .env has the server and Firebase values, the apps open a setup screen that names what is missing.
Build for release
flutter build appbundle # Android, for Google Play
flutter build ipa # iOS, for the App Store (on a Mac)
10. Run your marketplace
| Task | Where |
|---|---|
| Service areas | Admin → Zones: click the map to draw a zone; set it Live, Planned (customers see "coming soon") or Off, and whether instant jobs are allowed there. |
| Categories and services | Admin → Categories and Services. Pros add their own services under your categories; you can feature some in Admin → Featured. |
| Approve new pros | Admin → Providers → Pending: open the application, check the ID, insurance and licence documents, approve or ask for changes with a note the pro sees in the app. You can also add a pro yourself or import a CSV. |
| Bookings | Admin → Bookings: assign a pro, refund, cancel, open a dispute, or create a booking for a customer by phone. |
| Disputes and warranty claims | Admin → Disputes: read both sides with the job photos and chat, then refund, re-do or close. |
| Custom jobs | Admin → Custom jobs: jobs customers posted for bids. |
| Reviews | Admin → Reviews: reviews that share phone numbers or links are flagged automatically, and pros can report a review; keep or remove each one. |
| Banners, promo codes, gift codes, push campaigns | Admin → Banners, Promo codes, Notifications. |
| Taxes | Admin → Taxes: a rate per zone and/or category. The most specific active rule wins. |
| Staff and permissions | Admin → Roles: add staff and choose what each role may view and edit. The audit log shows who changed what. |
Pros can also work from the website: /pro is the pro web panel (bookings, calendar, services, staff, earnings, withdrawals, plan, messages, reviews).
11. Payments, commission and payouts
Customers pay by card, from their wallet, or in cash to the pro. Turn each method on or off, set the tip buttons and the wallet top-up amounts in Admin → Settings → Payment gateways.
Card payments: Stripe
- In the Stripe dashboard copy the Secret key into
STRIPE_SECRET_KEY. - Developers → Webhooks → Add endpoint: URL
https://your-domain.com/api/v1/webhooks/stripe, eventscheckout.session.completed,checkout.session.async_payment_succeeded,invoice.paidandcustomer.subscription.deleted. Copy the signing secret intoSTRIPE_WEBHOOK_SECRET. - Restart:
docker compose up -d.
A card booking is authorised, not charged, when it is booked (Stripe Checkout opens in the browser and returns to the app, and the card is saved with Stripe). The card is charged when the pro starts the job, after the customer's start code. Extra charges the customer approves and tips are charged to the same card afterwards (or taken from the wallet). A booking far in the future is authorised by the worker shortly before the job. Wallet top-ups use the same checkout.
Without a Stripe key, card payments run in demo mode with a DEMO label. Set DEMO_PAYMENTS=false to hide the card option until your key is in.
Commission and payouts
- Commission comes from the pro's plan (Admin → Plans) and can be overridden per category or per pro in Admin → Commissions.
- Card jobs add the pro's share to their balance when the job is finished. Cash jobs: the pro keeps the cash, and your commission, service fee and tax are added as a cash adjustment that is taken from their next payout.
- Payouts: pros request a withdrawal (minimum in Admin → Settings → Payment gateways). On your payout days, Admin → Payouts lists the requests with what to send after any cash debt. Approve, send the money from your bank, then mark each one paid with the bank reference; a failed payout returns the money to the pro's balance. The export button gives a CSV for your bank.
12. Pro plans
Plans (Admin → Plans) set a pro's commission and extras such as more staff and promo codes. Pros upgrade in the Pro app or in the web panel.
In the Pro app: RevenueCat (App Store and Google Play)
- Create one auto-renewing subscription per paid plan in App Store Connect and the Play Console.
- Create a project in RevenueCat, add both Pro apps, and import the products.
- Put each plan's product id in Admin → Plans → Edit → App store product id.
- App keys:
REVENUECAT_ANDROID_KEYandREVENUECAT_IOS_KEYinhandypro_provider_flutter/.env(RevenueCat → API keys → public app keys). - Server keys in
deploy/.env:REVENUECAT_API_KEY(secret key), andREVENUECAT_WEBHOOK_SECRET— the same value you enter as the authorization header in RevenueCat → Integrations → Webhooks, with the URLhttps://your-domain.com/api/v1/webhooks/revenuecat. Purchases, renewals, cancellations and expiries reach the server through this webhook.
In the pro web panel: Stripe
With STRIPE_SECRET_KEY set, the web panel's Plan page opens Stripe Checkout. Create a monthly price for each plan in Stripe and put its id in Admin → Plans → Edit → Stripe price id.
Without keys, plan purchases are granted at once in demo mode; set DEMO_PURCHASES=false to switch that off.
13. Push notifications and SMS
New requests, booking updates, messages, payouts, review replies and admin campaigns are sent with Firebase Cloud Messaging through your FIREBASE_* service account — nothing else to set for Android. For iOS, upload an APNs key: Apple Developer → Keys → + (Apple Push Notifications service), then Firebase → Project settings → Cloud Messaging → Apple app configuration → Upload. In Xcode, both apps have the Push Notifications capability; add Background Modes → Remote notifications if you turned it off.
Every user also has an in-app notification inbox. The message texts are in Admin → Settings → SMS & email.
SMS (booking confirmations and "your pro is on the way" texts to customers): set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_FROM_NUMBER from the Twilio console, then switch on Send booking text messages in Admin → Settings → SMS & email.
14. Maps, zones and addresses
- Zones decide where customers can book and which pros they see. A pro also sets a base address and a travel radius during sign-up.
- Map tiles: OpenStreetMap's public tiles by default — fine to start, but their usage policy asks heavy users to use a provider. Set
MAP_TILE_URL(e.g. MapTiler:https://api.maptiler.com/maps/streets-v2/{z}/{x}/{y}.png?key=YOUR_KEY) andMAP_ATTRIBUTION. The apps and the website read them from the server. - Address search: with
GOOGLE_MAPS_API_KEY(Geocoding API enabled) the server uses Google; without it, OpenStreetMap's Nominatim (rate-limited, results cached). - Travel times are estimates from the straight-line distance and a road factor, so no paid routing API is needed.
15. Email
Set SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PASS (any provider: Amazon SES, Brevo, Mailgun, Postmark, your host's mail server). Use port 587, or 465 with SMTP_SECURE=true. Until SMTP is set, emails (receipts, staff invites) are printed to the web container's log (docker compose logs web).
16. Sign-in methods
| Who | Methods |
|---|---|
| Customer app | Phone number (SMS code), Google, Apple (iOS), and guest (browse and price services; sign in to book). |
| Handypro Pro app | Phone number, Google and Apple (iOS). New pros then fill in the 6-step application. |
| Website and pro web panel | Phone number and Google. |
| Admin | Email and password. |
Apple sign-in (required by Apple when an iOS app offers Google sign-in): in the Apple Developer account, enable Sign in with Apple for both app ids and add the capability in Xcode, then turn Apple on in Firebase → Authentication → Sign-in method.
17. Rename the apps and package ids
From the kit folder (the one holding handypro_customer_flutter; requires Node.js):
node tools/rename.mjs --id com.yourcompany.fixly --name "Fixly"
--idsets the customer app's Android package and iOS bundle id. The Pro app gets<id>.provider, or choose it with--pro-id. Android'sMainActivityfolder is moved andFIREBASE_IOS_BUNDLE_IDin each app's.envis updated.--namerenames the customer app, names the Pro app "Name Pro" (or choose with--pro-name), and replaces the brand name in the app and website texts.
Then register the new ids in Firebase (chapter 4). The name people see inside the apps and on the website also comes from Admin → Settings → General → App name, so you can change it any time without a new build.
18. Logo, icon, colours and fonts
- App icons and splash screens: each app keeps its images in
assets/branding/—icon.png(1024×1024, no transparency),icon_foreground.png(the mark on a transparent 1024×1024 canvas, for Android's adaptive icon),splash.pngandsplash_android12.png. Replace them, change the colours underflutter_launcher_iconsandflutter_native_splashinpubspec.yamlif you like, then run in that app:dart run flutter_launcher_icons dart run flutter_native_splash:create - Logo inside the apps:
assets/images/logo.pngin each app, or upload a logo in Admin → Settings → General, which the apps and website use instead. - Website icon:
handypro_web_nextjs/src/app/icon.png(256×256) andapple-icon.png(180×180). - Brand colour: Admin → Settings → General → Primary colour changes it in both apps and on the website without a new build. The default palette is in
handypro_flutter_core/lib/src/theme/tokens.dartandhandypro_web_nextjs/src/app/globals.css. - Fonts: the apps use Manrope from
handypro_flutter_core/assets/fonts/(declared in that package'spubspec.yaml); the website loads it from Google Fonts insrc/app/layout.tsx.
19. Basic edits
| I want to change… | Where |
|---|---|
| Terms, privacy policy, provider terms and About | Admin → Settings → Legal pages (shown in both apps and on the website). |
| Currency, distance unit, time zone, support email | Admin → Settings → General. |
| Service fee, instant jobs, slot length, cancel and reschedule fees, warranty days | Admin → Settings → Bookings. |
| Payment methods, tips, wallet amounts, payout days, referral rewards | Admin → Settings → Payment gateways. |
| Push, email and SMS texts | Admin → Settings → SMS & email. |
| Force an app update, store links | Admin → Settings → App versions. |
| Maintenance message, guest browsing | Admin → Settings → General. |
| Help centre questions | Admin → Settings → General. |
| Home banners | Admin → Banners. |
| Texts inside the apps | The screens are in lib/pages/ of each app (shared screens in handypro_flutter_core/lib/src/pages/); search for the text and edit it. |
| Sample images | Upload your own photos in the admin. The sample job photos are in handypro_web_nextjs/public/samples/. |
20. Every key, explained
Each folder has a .env.example that lists its keys with comments. This table lists all of them: website = handypro_web_nextjs/.env, Docker = deploy/.env, customer app / Pro app = each app's .env.
| Key | Required | Where | What it does |
|---|---|---|---|
| Database | |||
DATABASE_URL | Yes | web | PostgreSQL connection string |
POSTGRES_DB | Yes | Docker | Database the Docker stack creates |
POSTGRES_USER | Yes | Docker | Database user for the Docker stack |
POSTGRES_PASSWORD | Yes | Docker | Database password for the Docker stack |
| App | |||
APP_URL | Yes | web, Docker | Public URL of the website/API, e.g. https://handypro.example.com |
NEXT_PUBLIC_APP_NAME | — | web, Docker | Name shown before the admin sets one |
WEB_PORT | — | Docker | Host port the web container listens on |
DOMAIN | — | Docker | Your domain for automatic HTTPS (docker compose --profile https) |
CORS_ORIGINS | — | web, Docker | Browser apps allowed to call /api/v1 (Flutter web builds) |
DEMO_MODE | — | web, Docker | "true" only for a public demo: one-click demo sign-in |
| Firebase sign-in | |||
NEXT_PUBLIC_FIREBASE_API_KEY | Yes | web, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | web, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | web, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | — | web, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | — | web, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_APP_ID | Yes | web, Docker | Firebase web app config |
| Firebase Admin + push | |||
FIREBASE_PROJECT_ID | Yes | web, Docker | Firebase project id |
FIREBASE_CLIENT_EMAIL | Yes | web, Docker | Service account e-mail |
FIREBASE_PRIVATE_KEY | Yes | web, Docker | Service account private key |
| Payments | |||
STRIPE_SECRET_KEY | — | web, Docker | Stripe secret key — card payments (empty = demo card) |
STRIPE_WEBHOOK_SECRET | — | web, Docker | Stripe webhook signing secret (/api/v1/webhooks/stripe) |
DEMO_PAYMENTS | — | web, Docker | "false" hides the demo card when Stripe is not set |
| Pro plans | |||
REVENUECAT_API_KEY | — | web, Docker | RevenueCat secret API key — verifies plan purchases made in Handypro Pro (empty = demo purchases) |
REVENUECAT_WEBHOOK_SECRET | — | web, Docker | Authorization value you set on the RevenueCat webhook (/api/v1/webhooks/revenuecat) |
DEMO_PURCHASES | — | web, Docker | "false" turns off demo plan purchases when no RevenueCat/Stripe key is set |
| SMS | |||
TWILIO_ACCOUNT_SID | — | web, Docker | Twilio account SID — booking text messages (empty = push and inbox only) |
TWILIO_AUTH_TOKEN | — | web, Docker | Twilio auth token |
TWILIO_FROM_NUMBER | — | web, Docker | Your Twilio phone number, e.g. +15125550100 |
| Maps | |||
GOOGLE_MAPS_API_KEY | — | web, Docker | Google Geocoding for address search (empty = OpenStreetMap) |
MAP_TILE_URL | — | web, Docker | Map tiles URL template (MapTiler, Stadia, Mapbox…) |
MAP_ATTRIBUTION | — | web, Docker | Credit line your tile provider requires |
| Storage | |||
S3_ENDPOINT | — | web, Docker | S3-compatible endpoint (MinIO, R2); empty for AWS |
S3_REGION | — | web, Docker | Bucket region |
S3_BUCKET | — | web, Docker | Bucket for photos and documents (empty = local uploads folder) |
S3_ACCESS_KEY_ID | — | web, Docker | Storage access key |
S3_SECRET_ACCESS_KEY | — | web, Docker | Storage secret key |
SMTP_HOST | — | web, Docker | SMTP server; empty = emails are printed to the log |
SMTP_PORT | — | web, Docker | 587 (STARTTLS) or 465 (TLS) |
SMTP_USER | — | web, Docker | SMTP user |
SMTP_PASS | — | web, Docker | SMTP password |
SMTP_SECURE | — | web, Docker | "true" for port 465 (TLS); empty for 587 |
| Storage | |||
UPLOAD_DIR | — | web | Folder for uploads when no bucket is set (default ./uploads) |
FILES_SECRET | — | web, Docker | Signs the 1-hour links to pro documents (default: derived from DATABASE_URL) |
| Mobile apps | |||
API_BASE_URL | Yes | customer, provider | Your server; the app calls <url>/api/v1 |
APP_NAME | — | customer, provider | App name in the UI |
FIREBASE_PROJECT_ID | Yes | customer, provider | Firebase project id |
FIREBASE_MESSAGING_SENDER_ID | Yes | customer, provider | Firebase config |
FIREBASE_STORAGE_BUCKET | — | customer, provider | Firebase config |
FIREBASE_ANDROID_API_KEY | Yes | customer, provider | Firebase Android config |
FIREBASE_ANDROID_APP_ID | Yes | customer, provider | Firebase Android config |
FIREBASE_IOS_API_KEY | Yes | customer, provider | Firebase iOS config |
FIREBASE_IOS_APP_ID | Yes | customer, provider | Firebase iOS config |
FIREBASE_IOS_CLIENT_ID | — | customer, provider | Google sign-in on iOS |
FIREBASE_IOS_BUNDLE_ID | — | customer, provider | iOS bundle id |
GOOGLE_SERVER_CLIENT_ID | — | customer, provider | Web OAuth client id — Google sign-in on Android |
SUPPORT_EMAIL | — | customer, provider | Fallback support e-mail until the server config loads |
DEMO_SIGN_IN | — | customer, provider | "true" shows one-tap demo sign-in (your public demo only) |
REVENUECAT_ANDROID_KEY | — | provider | RevenueCat public SDK key for Google Play (plan purchases; empty = demo) |
REVENUECAT_IOS_KEY | — | provider | RevenueCat public SDK key for the App Store (plan purchases; empty = demo) |
21. File structure
Apps (handypro_customer_flutter/lib, handypro_provider_flutter/lib)
main.dart starts Firebase and the app
app.dart theme (with the admin's brand colour) and push handling
router.dart every screen's route and the start-up gates (setup → onboarding → sign-in → …)
pages/ one folder per area
customer: launch, location, home, catalog, search, providers, booking, bookings,
jobs (custom jobs and bids), wallet, chat, account
pro: launch, apply (sign-up), home, requests, jobs, calendar, services,
staff, leads, grow (promos, plans), earnings, reviews, chat, profile
providers/ app state with Riverpod
widgets/, utils/ app-only widgets and helpers
assets/branding/ icon and splash images
Shared app code (handypro_flutter_core/lib/src)
api/ API client
auth/ Firebase sign-in (phone, Google, Apple, guest)
config/ .env reading and Firebase options
models/ Booking, Service, Provider, Config…
pages/ shared screens (legal pages, maintenance, update required…)
providers/ shared Riverpod providers
theme/ colour tokens, text styles, light and dark themes
widgets/ buttons, cards, fields, sheets, map, states (Hp*)
utils/ formatting and helpers
Long lists use lazy .builder lists; state flows through Riverpod providers.
Server (handypro_web_nextjs/src)
app/(site)/ the public website (home, services, categories, pros, search, booking,
checkout, bookings, account, help, become a pro, legal pages)
app/pro/ the pro web panel
app/admin/ the admin panel
app/install/ the first-run wizard
app/api/v1/ one route that hands every API call to lib/server/router.ts
lib/server/ business logic: handlers/ (the API routes), bookings, pricing, slots, payments
(Stripe), wallet, notify (push), sms, email, storage, geo, settings, setup-check
lib/client/ browser code: Firebase sign-in, API calls
components/ site/ (website), panel/ (admin and pro panel), app/ (shared), ui/ (building blocks)
database/ Prisma schema, migrations, seed (sample data)
worker/ background jobs (pnpm worker)
tests/ API tests (pnpm test)
Outside src, scripts/doctor.ts is pnpm run doctor, and e2e/ holds browser smoke tests for a running server:
E2E_BASE_URL=https://your-domain.com pnpm e2e (run pnpm exec playwright install chromium once first).
22. Publish to the stores
You publish both apps under your own developer accounts. Rename them first (chapter 17). Repeat these steps for each app.
Google Play
- Create an upload key:
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload. - Create
android/key.propertiesin the app:storeFile=/home/you/upload-keystore.jks storePassword=… keyAlias=upload keyPassword=…storeFileis the full path to your keystore. The build uses it automatically (without it, release builds are signed with the debug key, which Google Play refuses). Never share this file or the keystore. - Set the version in
pubspec.yaml(version: 1.0.0+1), runflutter build appbundle, and uploadbuild/app/outputs/bundle/release/app-release.aabin the Play Console. - Fill the store listing, the data safety form (the apps collect name, phone, email, precise location while a pro travels to a job, approximate location while browsing, photos and documents the user uploads, payment info through Stripe, and messages) and the content rating. The Pro app shares location in the foreground only while a job is "On my way".
- Add the Play app signing key's SHA-1 and SHA-256 to Firebase (chapter 4).
App Store
- In Apple Developer, create the app id with Push Notifications and Sign in with Apple.
- Open
ios/Runner.xcworkspacein Xcode and choose your team under Signing & Capabilities. flutter build ipa, then upload with Xcode's Organizer or Transporter, and submit in App Store Connect. Give the reviewer a test phone number and code (chapter 4 tip). For the Pro app, also give them an approved test pro account, because new pros wait for your approval.
23. Updating
When a new version comes out, read its changelog, back up your database, and copy the new files over your copy (keep your .env files and your edits — a tool like git makes merging easy). Then docker compose up -d --build; migrations run on start. For the apps, run flutter pub get and build again.
24. FAQ and troubleshooting
An app shows the setup screen
Its .env is missing API_BASE_URL or Firebase values. Fill them in and restart the app (a hot reload does not re-read .env).
The app can't reach the server
Open API_BASE_URL/api/v1/health in the phone's browser. On the Android emulator, localhost is the emulator itself — use http://10.0.2.2:3000. Android blocks plain http:// to other hosts; use HTTPS.
Sign-in fails on the website
Add your domain under Firebase → Authentication → Settings → Authorized domains, and check the NEXT_PUBLIC_FIREBASE_* values in Admin → Setup check.
Google sign-in fails on Android
Add the SHA-1 and SHA-256 of the key that signed the build (debug, upload and Play signing) to that Android app in Firebase, and set GOOGLE_SERVER_CLIENT_ID.
Phone sign-in says the SMS can't be sent
Allow your users' countries in Firebase → Authentication → Settings → SMS region policy.
Customers see no pros
The address must be inside a Live zone (Admin → Zones), and pros must be approved, online or have open hours, and offer a service in that category within their travel radius.
A new pro can't get requests
Approve them in Admin → Providers → Pending. Then they need at least one active service and their hours; the Pro app home reminds them.
Card payments say "DEMO"
No Stripe key is set. See chapter 11. Plan purchases say DEMO without RevenueCat (app) or Stripe (web panel) keys, see chapter 12.
Photos or documents do not upload
Run the setup check: the storage test shows the problem (wrong S3_* keys, or the bucket does not exist).
Requests never expire, cards are not authorised, campaigns stay scheduled
The worker must be running: docker compose ps shows worker up, and Admin → Setup check shows its last heartbeat.
I changed .env and nothing happened
Server: docker compose up -d recreates the containers with the new values. Apps: stop and start the app again.
Where are the logs?
docker compose logs -f web and docker compose logs -f worker.
25. Credits
Handypro is built on these open-source projects, each under its own licence:
Apps
Flutter, flutter_riverpod, go_router, firebase_core, firebase_auth, firebase_messaging, google_sign_in, sign_in_with_apple, purchases_flutter, flutter_map, latlong2, geolocator, image_picker, http, flutter_dotenv, shared_preferences, share_plus, url_launcher, package_info_plus, intl, lucide_icons_flutter, flutter_launcher_icons, flutter_native_splash.
Website, panels and API
Next.js, React, Prisma, PostgreSQL, Tailwind CSS, shadcn/ui, Base UI, Lucide, Leaflet and React Leaflet, Firebase JS SDK and Firebase Admin, Stripe Node, AWS SDK for JavaScript, Nodemailer, Zod, Sonner, next-themes, node-qrcode, MinIO, Caddy.
Fonts, maps and content
Manrope (SIL Open Font License 1.1, licence file in handypro_flutter_core/assets/fonts/OFL.txt). Map data © OpenStreetMap contributors. The sample businesses, people, reviews and texts are original and fictional. The sample images are placeholders; the photos on the live demo are not part of the download.
26. Changelog
1.0.0 — 2026-10-07
First release. The full list is in CHANGELOG.md.
Not in this version
Video estimates, recurring bookings, app languages other than English, and two-step sign-in for admins.
27. Support
Use the Support tab on the item's CodeCanyon page. Please include your purchase code, what you did, what you expected and what happened, and the output of pnpm run doctor or a screenshot of Admin → Setup check.
Support covers questions about the item's features and fixing bugs in the item. Custom changes and installation on your server are not part of item support.