Estatly

A real-estate marketplace for your city: one Flutter app (iOS and Android) for buyers, renters, owners and agents, and one Next.js server that is the website, the admin panel and the API. You add your own keys; everything else is ready.

1. Welcome

Thank you for buying Estatly. This guide is written for beginners: follow the chapters in order and you will have the server, the website, the admin panel and the app running under your own name and keys.

What is in the download

FolderWhat it is
estatly_mobile_flutter/The app. People search homes for sale and rent (list, map, draw on map), save searches and homes, book visits, chat with agents and owners, and post their own listings with packages and boosts.
estatly_web_nextjs/One Next.js app that is the website, the admin panel (/admin), the API the app uses (/api/v1) and the background worker (pnpm worker).
deploy/The Docker stack: website/API + worker + PostgreSQL database + MinIO file storage, started with one command.
tools/rename.mjsRenames the app, its package id, brand colour, website font and icon in one command.
docs/This documentation (HTML and PDF).

How the parts fit

The app and the website talk to the same server. All data (listings, projects, people, enquiries, visits, payments) lives in your PostgreSQL database. Photos, tour videos and verification 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 listing: an owner or agent posts it in the 8-step wizard → it waits in Admin → Properties → In review (verified sellers can skip review) → once approved it shows in search, on the map and in saved-search alerts → buyers send enquiries and book visits → the seller marks it sold or rented, or it expires after the days its package allows.

Nothing is blocked on a key. Without RevenueCat or Stripe keys, packages and boosts are granted at once with a DEMO label so you can test. Without SMTP, emails are printed to the server log. Without a Google key, address search uses OpenStreetMap. Without an AI key, a built-in writer composes listing descriptions from the listing's facts.

2. Requirements

To run the server

To build the mobile app

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 estatly-1.0.0.zip root@YOUR_SERVER_IP:, then on the server:
    apt install -y unzip
    unzip estatly-1.0.0.zip
    cd estatly/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_SECRETA third random value (openssl rand -hex 32). It signs the short-lived links to private files.
    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.
  4. Open APP_URL/install in your browser. The install wizard creates your admin account, names the marketplace, sets the currency and units, and loads the sample marketplace or just your first city (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 (Google, Apple, phone, guest, and email for admin staff) and push notifications. Your data stays in your own database.

  1. Go to the Firebase console → Add project. Give it your app name.
  2. Build → Authentication → Get started → Sign-in method. Turn on: Anonymous (guest mode), Google, Phone, Email/Password (admin staff) and Apple (see chapter 16).
  3. Phone sign-in only: 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 mobile app: Project settings → Your apps → Add app → Android and iOS, with your package id (e.g. com.yourcompany.estatly). Rename the app first if you want your own id (chapter 17). Or use the FlutterFire CLI, which registers both for you:
    dart pub global activate flutterfire_cli
    cd estatly_mobile_flutter
    flutterfire configure --project your-project --platforms android,ios \
      --android-package-name com.yourcompany.estatly --ios-bundle-id com.yourcompany.estatly
  8. Copy the app's values into estatly_mobile_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 the app's google-services.json / GoogleService-Info.plist (download them from Project settings), or on the app's card. The app reads these values from .env, so you do not need to put the downloaded files in the project.
  9. Google sign-in on Android needs GOOGLE_SERVER_CLIENT_ID in the app's .env: Authentication → Sign-in method → Google → Web SDK configuration → Web client ID.
  10. Google sign-in on iOS: open estatly_mobile_flutter/ios/Flutter/GoogleSignIn.xcconfig and set GOOGLE_IOS_CLIENT_ID (the CLIENT_ID in GoogleService-Info.plist) and GOOGLE_REVERSED_CLIENT_ID (its REVERSED_CLIENT_ID).
  11. Android Google and phone sign-in need your signing key fingerprints: run cd android && ./gradlew signingReport in the 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 estatly estatly | gzip > /root/backups/estatly-$(date +%F).sql.gz

Also back up the minio-data volume (photos, videos and 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 estatly_web_nextjs/ into Vercel. Add every key from estatly_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 estatly_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, listings do not expire, saved-search alerts and visit reminders are not sent, and scheduled notifications 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; verification documents are only reachable through short-lived signed links.

Without Docker

Install Node.js 22 and pnpm 11, then in estatly_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). 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. The marketplace: name, currency, brand colour, area unit (square feet or metres), distance unit and time zone.
  3. The content: Load the sample marketplace for sample homes, projects, agents, reviews, articles, cities and neighbourhoods around Austin, Texas, so every screen has content — or Start empty with only the basics (packages, boosts, property types, facilities) and your first city.

The wizard closes for good once a super admin exists. Sign in later at /admin. The admin sign-in page links to the wizard while no admin exists.

The sample people, listings and texts are fictional, and the sample images are placeholders. Delete or edit them in the admin when you add real ones; replace a city's position in Admin → Cities.

8. Setup check

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

9. Run the mobile app

  1. Copy the settings file and fill it in:
    cd estatly_mobile_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.
  2. Get the packages and run:
    flutter pub get
    flutter run

Until .env has the server and Firebase values, the app opens a "Finish the app 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
Cities and neighbourhoodsAdmin → Cities. Add a city by clicking its centre on the map; draw each neighbourhood on the map and enter its scores (walk, transit, schools, safety…). Listings inside an outline get that neighbourhood automatically.
Review new listingsAdmin → Properties → In review: open one, go through the checks, approve (optionally featured) or reject with a reason the seller sees. Admin → Settings → Listings turns on auto-approve for verified sellers and sets when a price edit needs review again.
Verify agents and ownersAdmin → Verification: compare the ID, selfie and documents, tick the checklist, then approve, reject or ask for more. Approved sellers get the Verified badge. Documents are deleted 30 days after the decision.
ReportsAdmin → Reports lists listings people flagged. Take one down with a reason, or dismiss the reports.
New-build projectsAdmin → Projects: add a development with its photos, stage and floor plans.
Property types and fieldsAdmin → Types & categories and Custom fields (extra details like parking or HOA fee, and the facilities list).
Invite agentsAdmin → Agents & owners → Invite agent. They get an email and become an agent when they sign in with that email.
Import listingsAdmin → Properties → Import CSV (the dialog shows the columns).
Banners, articles, push campaignsAdmin → Sliders & banners, Articles, Notifications.
Staff and permissionsAdmin → Roles: add staff (moderator, finance, content editor or super admin) and choose what each role may view, edit, approve and delete. The Activity log shows who changed what.

11. Packages, boosts and payments

Owners and agents can list a few homes for free. Packages (Admin → Packages) raise how many listings can be live, for how long, and add boosts, analytics, lead export and priority review. A boost puts one listing at the top of search and on Home for 3, 7 or 14 days. The server enforces every limit.

In the app: RevenueCat (App Store and Google Play)

  1. Create the products in App Store Connect and the Play Console: one auto-renewing subscription per package and period (monthly, yearly) and one consumable per boost.
  2. Create a project in RevenueCat, add both apps, and import the products.
  3. Put the product ids in Admin → Packages (each package's store product ids) and on each boost.
  4. App keys: REVENUECAT_APPLE_KEY and REVENUECAT_GOOGLE_KEY in estatly_mobile_flutter/.env (RevenueCat → API keys → public app keys).
  5. Server keys in deploy/.env: REVENUECAT_SECRET_KEY, and REVENUECAT_WEBHOOK_AUTH — the same value you enter in RevenueCat → Integrations → Webhooks, with the URL https://your-domain.com/api/v1/webhooks/revenuecat. Renewals, cancellations and expiries reach the server through this webhook.

On the website: Stripe Checkout

  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, invoice.paid, customer.subscription.updated and customer.subscription.deleted. Copy the signing secret into STRIPE_WEBHOOK_SECRET.
  3. Optional: create Stripe prices for each package and put their ids in Admin → Packages (Stripe price · monthly / yearly). Without them, Checkout uses the package's price from the admin.
  4. Restart: docker compose up -d. Members manage their card and invoices through Stripe's billing portal from Account → Plan & billing.

Bank transfer and demo purchases

Admin → Settings → Payment gateways: turn on bank transfer to show your bank details at checkout; the payment waits in Admin → Payments → Pending until you approve it. While no gateway is connected, purchases are granted at once with a DEMO label; set DEMO_PURCHASES=false to switch that off before your keys are in. Refunds: Admin → Payments → a payment → Refund (Stripe refunds go through Stripe; App Store and Google Play refunds happen in their own consoles and are only recorded here).

Charging agents or owners through your product requires the Envato Extended Licence.

12. Push notifications

New enquiries and messages, visit updates, listing decisions, saved-search alerts 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, the app already has the Push Notifications capability; add Background Modes → Remote notifications if you turned it off.

Every user also has an in-app notification inbox, quiet hours, and switches for each kind of notification in the app's settings.

13. Maps and addresses

14. AI description writer

In the post wizard, sellers can write the description with one tap. Set AI_API_KEY for any OpenAI-compatible API (OpenAI, OpenRouter, Groq, or a local Ollama with AI_BASE_URL), and optionally AI_MODEL. Without a key, a built-in writer composes the text from the listing's facts. The number of generations per person per day is in Admin → Settings → Listings.

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. The sender name and address, and which events also send an email, are in Admin → Settings → Email & SMS, with a Send a test email button. Until SMTP is set, emails are printed to the web container's log (docker compose logs web); staff invites then show the set-password link in the admin so you can pass it on.

16. Sign-in methods

WhoMethods
AppGoogle, Apple (iOS), phone number (SMS code), and guest (browse, save and search; sign in to post, chat or book).
WebsiteGoogle, Apple and phone number.
AdminEmail and password, with "Forgot password".

Switch methods on or off in Admin → Settings → Sign-in & app versions (they must also be on in Firebase).

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 your app id and add the capability in Xcode. For the website, create a Services ID and a key, and enter them in Firebase → Authentication → Sign-in method → Apple, with the return URL Firebase shows there.

17. Rename the app and package id

From the kit folder (the one holding estatly_mobile_flutter; requires Node.js):

node tools/rename.mjs --name "HomeHub" --id com.yourcompany.homehub

Then register the new id in Firebase (chapter 4). The name people see inside the app and on the website also comes from Admin → Settings → General → App name, so you can change it any time without a new build.

The rename tool does these too:

node tools/rename.mjs --color "#0F766E" --font "Manrope" --icon my-icon.png

19. Basic edits

I want to change…Where
Terms, privacy policy and AboutAdmin → Settings → General → Legal pages (shown in the app and on the website).
Currency, area and distance units, time zoneAdmin → Settings → General → Region.
Listing rules (photos, title length, review)Admin → Settings → Listings.
Mortgage calculator defaultsAdmin → Settings → Listings → Mortgage calculator defaults.
Home bannersAdmin → Sliders & banners (app home slider and website hero, with schedule and city).
Help centre questionsAdmin → Settings → General → Help centre questions.
Support email, phone and officeAdmin → Settings → General → Support.
Force an app update, maintenance messageAdmin → Settings → Sign-in & app versions (minimum versions) and General → Access (maintenance mode).
App download links on the websiteAdmin → Settings → Sign-in & app versions (App Store, Google Play and APK links).
Texts inside the appThe screens are in estatly_mobile_flutter/lib/pages/; search for the text and edit it.
Sample imagesUpload your own photos to listings, projects, banners and articles in the admin. The onboarding and sign-in pictures in the app are in estatly_mobile_flutter/assets/images/.

20. Every key, explained

Each folder has a .env.example that lists its keys with comments. This table lists all of them: website = estatly_web_nextjs/.env, Docker = deploy/.env, app = estatly_mobile_flutter/.env.

KeyRequiredWhereWhat it does
Database
DATABASE_URLYeswebsitePostgreSQL connection string
POSTGRES_DBYesDockerDatabase the Docker stack creates
POSTGRES_USERYesDockerDatabase user for the Docker stack
POSTGRES_PASSWORDYesDockerDatabase password for the Docker stack
App
APP_URLYeswebsite, DockerPublic URL of the website/API, e.g. https://homes.example.com
APP_SECRET—website, DockerLong random string that signs private file links (verification documents)
NEXT_PUBLIC_APP_NAME—website, 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—website, DockerBrowser apps allowed to call /api/v1 (Flutter web builds)
DEMO_MODE—website, Docker"true" only for a public demo: one-click demo sign-in, read-only admin
Firebase sign-in
NEXT_PUBLIC_FIREBASE_API_KEYYeswebsite, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYeswebsite, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_PROJECT_IDYeswebsite, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET—website, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID—website, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_APP_IDYeswebsite, DockerFirebase web app config
Firebase Admin + push
FIREBASE_PROJECT_IDYeswebsite, DockerFirebase project id
FIREBASE_CLIENT_EMAILYeswebsite, DockerService account e-mail
FIREBASE_PRIVATE_KEYYeswebsite, DockerService account private key
Payments
REVENUECAT_SECRET_KEY—website, DockerRevenueCat secret API key — real App Store / Google Play purchases (empty = demo)
REVENUECAT_WEBHOOK_AUTH—website, DockerAuthorization header RevenueCat sends to /api/v1/webhooks/revenuecat
STRIPE_SECRET_KEY—website, DockerStripe secret key — website checkout (empty = demo)
STRIPE_WEBHOOK_SECRET—website, DockerStripe webhook signing secret (/api/v1/webhooks/stripe)
DEMO_PURCHASES—website, Docker"false" turns demo purchases off before store keys are set
Maps
GOOGLE_MAPS_API_KEY—website, DockerGoogle Geocoding for address search (empty = OpenStreetMap)
MAPBOX_TOKEN—website, DockerMapbox public token, when Settings → Maps uses Mapbox tiles
MAP_TILE_URL—website, DockerAny other tile URL template (MapTiler, Stadia…); overrides the setting
MAP_ATTRIBUTION—website, DockerCredit line your tile provider requires
AI description writer
AI_API_KEY—website, DockerKey for any OpenAI-compatible API (empty = built-in sample writer)
AI_BASE_URL—website, DockerAPI base URL (default https://api.openai.com/v1; OpenRouter, Groq, Ollama…)
AI_MODEL—website, DockerModel name (default gpt-4o-mini)
Storage
S3_ENDPOINT—website, DockerS3-compatible endpoint (MinIO, R2); empty for AWS
S3_REGION—website, DockerBucket region
S3_BUCKET—website, DockerBucket for photos, videos and documents (empty = local uploads folder)
S3_ACCESS_KEY_ID—website, DockerStorage access key
S3_SECRET_ACCESS_KEY—website, DockerStorage secret key
UPLOAD_DIR—websiteFolder for uploads when no bucket is set (default ./uploads)
Email
SMTP_HOST—website, DockerSMTP server; empty = emails are printed to the log
SMTP_PORT—website, Docker587 (STARTTLS) or 465 (TLS)
SMTP_USER—website, DockerSMTP user
SMTP_PASS—website, DockerSMTP password
SMTP_SECURE—website, Docker"true" for port 465 (TLS); empty for 587
Mobile app
API_BASE_URLYesappYour server; the app calls <url>/api/v1
APP_NAME—appApp name in the UI
FIREBASE_PROJECT_IDYesappFirebase project id
FIREBASE_MESSAGING_SENDER_IDYesappFirebase config
FIREBASE_STORAGE_BUCKET—appFirebase config
FIREBASE_ANDROID_API_KEYYesappFirebase Android config
FIREBASE_ANDROID_APP_IDYesappFirebase Android config
FIREBASE_IOS_API_KEYYesappFirebase iOS config
FIREBASE_IOS_APP_IDYesappFirebase iOS config
FIREBASE_IOS_CLIENT_ID—appGoogle sign-in on iOS
FIREBASE_IOS_BUNDLE_ID—appiOS bundle id
GOOGLE_SERVER_CLIENT_ID—appWeb OAuth client id — Google sign-in on Android
REVENUECAT_APPLE_KEY—appRevenueCat public SDK key for iOS (empty = demo purchases)
REVENUECAT_GOOGLE_KEY—appRevenueCat public SDK key for Android (empty = demo purchases)
SUPPORT_EMAIL—appFallback support e-mail until the server config loads
DEMO_SIGN_IN—app"true" shows one-tap demo sign-in (your public demo only)

21. File structure

Mobile app (estatly_mobile_flutter/lib)

main.dart            starts Firebase and the app
app.dart             theme and push handling
router.dart          every screen's route and the start-up gates (setup → onboarding → city → sign-in)
pages/               one folder per area: launch, auth, home, search, listing, projects, agents, saved,
                     visits, chats, post (the 8-step wizard), my_listings, billing, verification,
                     notifications, articles, profile
components/          shared widgets (cc_*): buttons, cards, listing card, map, sheets, states, stepper
providers/           app state with Riverpod (session, search filters, saved homes, data from the API)
services/            API client, sign-in, push, purchases (RevenueCat), local storage
models/              Listing, Project, Agent, Visit, Config…
config/              .env reading and Firebase options
theme/               colour tokens, text styles, light and dark themes
utils/               formatting and helpers

Long lists use lazy .builder lists; state flows through Riverpod providers.

Server (estatly_web_nextjs/src)

app/(site)/          the public website (home, search, homes, projects, agents, blog, pricing, sell, account)
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), listings, billing, payments (Stripe,
                     RevenueCat), notify (push), email, storage, geo, ai, settings, setup-check
lib/client/          browser code: Firebase sign-in, API calls, billing
components/          site/ (website), admin/ (admin panel), app/ (shared), ui/ (building blocks)
database/            Prisma schema, migrations, seed (sample data)
worker/              background jobs (pnpm worker)
scripts/doctor.ts    pnpm run doctor
tests/               API tests (pnpm test)

Outside src, 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 the app under your own developer accounts. Rename it first (chapter 17).

Google Play

  1. Create an upload key: keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload.
  2. Create estatly_mobile_flutter/android/key.properties:
    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 app collects name, phone, email, approximate location while searching, photos and documents the user uploads, and messages) and the content rating.
  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), and explain that posting a listing needs an account.

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 app, run flutter pub get and build again.

24. FAQ and troubleshooting

The app shows "Finish the app setup"

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 the 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.

A new listing does not show in search

It waits for review in Admin → Properties → In review (or turn on auto-approve for verified sellers in Admin → Settings → Listings).

Purchases say "DEMO"

No RevenueCat (app) or Stripe (website) key is set. See chapter 11.

Photos do not upload

Run the setup check: the storage test shows the problem (wrong S3_* keys, or the bucket does not exist).

Saved-search alerts and visit reminders never arrive

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. App: stop and start the app again.

Where are the logs?

docker compose logs -f web and docker compose logs -f worker.

25. Credits

Estatly is built on these open-source projects, each under its own licence:

Mobile app

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, video_player, cached_network_image, fl_chart, hive_ce, http, flutter_dotenv, shared_preferences, path_provider, connectivity_plus, share_plus, url_launcher, add_2_calendar, in_app_review, package_info_plus, intl, lucide_icons_flutter.

Website, admin 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

DM Sans (SIL Open Font License 1.1, licence file in estatly_mobile_flutter/assets/fonts/OFL.txt). Map data © OpenStreetMap contributors. The sample listings, people, reviews and texts are original and fictional. The sample images are generated 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

360° panorama tours, live in-app video visits (a virtual open house uses a meeting link instead), 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.