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

FolderWhat 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.mjsRenames both apps, their package ids and the brand name in one command.
docs/This documentation (HTML and PDF).
Customer app home
Customer app
Handypro Pro home
Handypro Pro app
Admin dashboard
Admin panel

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.

Nothing is blocked on a key. Without Stripe, card payments run in demo mode with a DEMO label. Without RevenueCat, pro plans are granted at once in demo mode. Without SMTP, emails are printed to the server log. Without Twilio, customers get push and inbox updates instead of SMS. Without a Google key, address search uses OpenStreetMap.

2. Requirements

To run the server

To build the apps

Accounts you will create (all have free tiers)

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.

  1. 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
  2. In .env, fill at least these values:
    KeyWhat to put
    POSTGRES_PASSWORDA long random password, letters and digits only (run openssl rand -hex 24 to 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 (or http://YOUR_SERVER_IP:3000 for a first test).
    NEXT_PUBLIC_FIREBASE_*, FIREBASE_*From chapter 4.
  3. Start everything:
    docker compose up -d --build
    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.
  4. Open APP_URL/install in 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).
  5. Check it: APP_URL/api/v1/health shows {"ok":true,…}. Then put it on your domain with HTTPS (chapter 5).
Changed .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.

  1. Go to the Firebase console → Add project. Give it your business name.
  2. 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).
  3. Phone sign-in: Authentication → Settings → SMS region policy — allow the countries your users are in. Without it Firebase refuses to send the code.
  4. Authentication → Settings → Authorized domains: add your-domain.com.
  5. Project settings (gear) → General → Your apps → Add app → Web (name it "Website"). Copy the values from the firebaseConfig shown into deploy/.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:…"
  6. Project settings → Service accounts → Generate new private key. A JSON file downloads. Copy three values from it into deploy/.env:
    FIREBASE_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"
    Keep the \n as they are in the file. Keep this file secret.
  7. 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
  8. Copy each app's values into its own .env (handypro_customer_flutter/.env and handypro_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's google-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.
  9. Google sign-in on Android needs GOOGLE_SERVER_CLIENT_ID in both apps' .env: Authentication → Sign-in method → Google → Web SDK configuration → Web client ID.
  10. iOS (Google and phone sign-in): in each app, copy ios/Flutter/Firebase.xcconfig.example to ios/Flutter/Firebase.xcconfig and fill in GOOGLE_REVERSED_CLIENT_ID (the REVERSED_CLIENT_ID in that app's GoogleService-Info.plist) and FIREBASE_ENCODED_APP_ID (its GOOGLE_APP_ID with : replaced by - and app- in front, as the example shows).
  11. Android Google and phone sign-in need your signing key fingerprints: run cd android && ./gradlew signingReport in 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.
Testing phone sign-in without SMS costs: Authentication → Sign-in method → Phone → Phone numbers for testing, add a number and a code.

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.

  1. Point your domain's DNS A record at the server and wait until it resolves.
  2. In deploy/.env set DOMAIN=your-domain.com and APP_URL=https://your-domain.com.
  3. 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
  4. Add your-domain.com to 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

  1. Create a PostgreSQL database (Neon, Supabase, Railway…) and copy its connection string.
  2. Import handypro_web_nextjs/ into Vercel. Add every key from handypro_web_nextjs/.env.example as an environment variable, with DATABASE_URL set to your database and the five S3_* keys set to a cloud bucket (Vercel has no disk for uploads).
  3. On your computer, create the tables once: cd handypro_web_nextjs && pnpm install && pnpm prisma:migrate:deploy (with DATABASE_URL in .env). Then open /install on your Vercel domain.
  4. The background worker does not run on Vercel. Run pnpm worker on 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:

  1. Your admin account (name, email and password). This is the super admin; you add more staff later in Admin → Roles.
  2. 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.
  3. 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.

The sample people, businesses, reviews and texts are fictional, and the sample images are placeholders. Delete or edit them in the admin when you add real ones. Draw your own service area in Admin → Zones.

8. Setup check

Two ways to see which keys are missing and whether each service answers:

9. Run the two apps

Do this for handypro_customer_flutter and for handypro_provider_flutter:

  1. Copy the settings file and fill it in:
    cd handypro_customer_flutter
    cp .env.example .env
    Set API_BASE_URL to your server (https://your-domain.com; for a server on your computer use http://10.0.2.2:3000 in the Android emulator) and the Firebase values from chapter 4. The Pro app also takes REVENUECAT_ANDROID_KEY and REVENUECAT_IOS_KEY (chapter 12).
  2. 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

TaskWhere
Service areasAdmin → 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 servicesAdmin → Categories and Services. Pros add their own services under your categories; you can feature some in Admin → Featured.
Approve new prosAdmin → 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.
BookingsAdmin → Bookings: assign a pro, refund, cancel, open a dispute, or create a booking for a customer by phone.
Disputes and warranty claimsAdmin → Disputes: read both sides with the job photos and chat, then refund, re-do or close.
Custom jobsAdmin → Custom jobs: jobs customers posted for bids.
ReviewsAdmin → 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 campaignsAdmin → Banners, Promo codes, Notifications.
TaxesAdmin → Taxes: a rate per zone and/or category. The most specific active rule wins.
Staff and permissionsAdmin → 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

  1. In the Stripe dashboard copy the Secret key into STRIPE_SECRET_KEY.
  2. Developers → Webhooks → Add endpoint: URL https://your-domain.com/api/v1/webhooks/stripe, events checkout.session.completed, checkout.session.async_payment_succeeded, invoice.paid and customer.subscription.deleted. Copy the signing secret into STRIPE_WEBHOOK_SECRET.
  3. 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

Charging customers or pros through your product requires the Envato Extended Licence.

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)

  1. Create one auto-renewing subscription per paid plan in App Store Connect and the Play Console.
  2. Create a project in RevenueCat, add both Pro apps, and import the products.
  3. Put each plan's product id in Admin → Plans → Edit → App store product id.
  4. App keys: REVENUECAT_ANDROID_KEY and REVENUECAT_IOS_KEY in handypro_provider_flutter/.env (RevenueCat → API keys → public app keys).
  5. Server keys in deploy/.env: REVENUECAT_API_KEY (secret key), and REVENUECAT_WEBHOOK_SECRET — the same value you enter as the authorization header in RevenueCat → Integrations → Webhooks, with the URL https://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

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

WhoMethods
Customer appPhone number (SMS code), Google, Apple (iOS), and guest (browse and price services; sign in to book).
Handypro Pro appPhone number, Google and Apple (iOS). New pros then fill in the 6-step application.
Website and pro web panelPhone number and Google.
AdminEmail 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"

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.

19. Basic edits

I want to change…Where
Terms, privacy policy, provider terms and AboutAdmin → Settings → Legal pages (shown in both apps and on the website).
Currency, distance unit, time zone, support emailAdmin → Settings → General.
Service fee, instant jobs, slot length, cancel and reschedule fees, warranty daysAdmin → Settings → Bookings.
Payment methods, tips, wallet amounts, payout days, referral rewardsAdmin → Settings → Payment gateways.
Push, email and SMS textsAdmin → Settings → SMS & email.
Force an app update, store linksAdmin → Settings → App versions.
Maintenance message, guest browsingAdmin → Settings → General.
Help centre questionsAdmin → Settings → General.
Home bannersAdmin → Banners.
Texts inside the appsThe 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 imagesUpload 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.

KeyRequiredWhereWhat it does
Database
DATABASE_URLYeswebPostgreSQL connection string
POSTGRES_DBYesDockerDatabase the Docker stack creates
POSTGRES_USERYesDockerDatabase user for the Docker stack
POSTGRES_PASSWORDYesDockerDatabase password for the Docker stack
App
APP_URLYesweb, DockerPublic URL of the website/API, e.g. https://handypro.example.com
NEXT_PUBLIC_APP_NAME—web, DockerName shown before the admin sets one
WEB_PORT—DockerHost port the web container listens on
DOMAIN—DockerYour domain for automatic HTTPS (docker compose --profile https)
CORS_ORIGINS—web, DockerBrowser 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_KEYYesweb, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYesweb, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_PROJECT_IDYesweb, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET—web, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID—web, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_APP_IDYesweb, DockerFirebase web app config
Firebase Admin + push
FIREBASE_PROJECT_IDYesweb, DockerFirebase project id
FIREBASE_CLIENT_EMAILYesweb, DockerService account e-mail
FIREBASE_PRIVATE_KEYYesweb, DockerService account private key
Payments
STRIPE_SECRET_KEY—web, DockerStripe secret key — card payments (empty = demo card)
STRIPE_WEBHOOK_SECRET—web, DockerStripe 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, DockerRevenueCat secret API key — verifies plan purchases made in Handypro Pro (empty = demo purchases)
REVENUECAT_WEBHOOK_SECRET—web, DockerAuthorization 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, DockerTwilio account SID — booking text messages (empty = push and inbox only)
TWILIO_AUTH_TOKEN—web, DockerTwilio auth token
TWILIO_FROM_NUMBER—web, DockerYour Twilio phone number, e.g. +15125550100
Maps
GOOGLE_MAPS_API_KEY—web, DockerGoogle Geocoding for address search (empty = OpenStreetMap)
MAP_TILE_URL—web, DockerMap tiles URL template (MapTiler, Stadia, Mapbox…)
MAP_ATTRIBUTION—web, DockerCredit line your tile provider requires
Storage
S3_ENDPOINT—web, DockerS3-compatible endpoint (MinIO, R2); empty for AWS
S3_REGION—web, DockerBucket region
S3_BUCKET—web, DockerBucket for photos and documents (empty = local uploads folder)
S3_ACCESS_KEY_ID—web, DockerStorage access key
S3_SECRET_ACCESS_KEY—web, DockerStorage secret key
Email
SMTP_HOST—web, DockerSMTP server; empty = emails are printed to the log
SMTP_PORT—web, Docker587 (STARTTLS) or 465 (TLS)
SMTP_USER—web, DockerSMTP user
SMTP_PASS—web, DockerSMTP password
SMTP_SECURE—web, Docker"true" for port 465 (TLS); empty for 587
Storage
UPLOAD_DIR—webFolder for uploads when no bucket is set (default ./uploads)
FILES_SECRET—web, DockerSigns the 1-hour links to pro documents (default: derived from DATABASE_URL)
Mobile apps
API_BASE_URLYescustomer, providerYour server; the app calls <url>/api/v1
APP_NAME—customer, providerApp name in the UI
FIREBASE_PROJECT_IDYescustomer, providerFirebase project id
FIREBASE_MESSAGING_SENDER_IDYescustomer, providerFirebase config
FIREBASE_STORAGE_BUCKET—customer, providerFirebase config
FIREBASE_ANDROID_API_KEYYescustomer, providerFirebase Android config
FIREBASE_ANDROID_APP_IDYescustomer, providerFirebase Android config
FIREBASE_IOS_API_KEYYescustomer, providerFirebase iOS config
FIREBASE_IOS_APP_IDYescustomer, providerFirebase iOS config
FIREBASE_IOS_CLIENT_ID—customer, providerGoogle sign-in on iOS
FIREBASE_IOS_BUNDLE_ID—customer, provideriOS bundle id
GOOGLE_SERVER_CLIENT_ID—customer, providerWeb OAuth client id — Google sign-in on Android
SUPPORT_EMAIL—customer, providerFallback 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—providerRevenueCat public SDK key for Google Play (plan purchases; empty = demo)
REVENUECAT_IOS_KEY—providerRevenueCat 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

  1. Create an upload key: keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload.
  2. Create android/key.properties in the app:
    storeFile=/home/you/upload-keystore.jks
    storePassword=…
    keyAlias=upload
    keyPassword=…
    storeFile is 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.
  3. Set the version in pubspec.yaml (version: 1.0.0+1), run flutter build appbundle, and upload build/app/outputs/bundle/release/app-release.aab in the Play Console.
  4. 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".
  5. Add the Play app signing key's SHA-1 and SHA-256 to Firebase (chapter 4).

App Store

  1. In Apple Developer, create the app id with Push Notifications and Sign in with Apple.
  2. Open ios/Runner.xcworkspace in Xcode and choose your team under Signing & Capabilities.
  3. 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.