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
| Folder | What 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.mjs | Renames 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.
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 mobile app
- Flutter 3.44 or newer (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: RevenueCat (in-app purchases in the app), Stripe (card payments on the website), Google Cloud (address search with the Geocoding API), a map tile provider such as MapTiler or Mapbox, an OpenAI-compatible AI API, 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 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 - 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_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(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.docker compose up -d --build - Open
APP_URL/installin 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). - 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 (Google, Apple, phone, guest, and email for admin staff) and push notifications. Your data stays in your own database.
- Go to the Firebase console → Add project. Give it your app name.
- Build → Authentication → Get started → Sign-in method. Turn on: Anonymous (guest mode), Google, Phone, Email/Password (admin staff) and Apple (see chapter 16).
- Phone sign-in only: 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 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 - 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'sgoogle-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. - Google sign-in on Android needs
GOOGLE_SERVER_CLIENT_IDin the app's.env: Authentication → Sign-in method → Google → Web SDK configuration → Web client ID. - Google sign-in on iOS: open
estatly_mobile_flutter/ios/Flutter/GoogleSignIn.xcconfigand setGOOGLE_IOS_CLIENT_ID(theCLIENT_IDinGoogleService-Info.plist) andGOOGLE_REVERSED_CLIENT_ID(itsREVERSED_CLIENT_ID). - Android Google and phone sign-in need your signing key fingerprints: run
cd android && ./gradlew signingReportin 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.
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 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
- Create a PostgreSQL database (Neon, Supabase, Railway…) and copy its connection string.
- Import
estatly_web_nextjs/into Vercel. Add every key fromestatly_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 estatly_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, 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:
- Your admin account (name, email and password). This is the super admin; you add more staff later in Admin → Roles.
- The marketplace: name, currency, brand colour, area unit (square feet or metres), distance unit and time zone.
- 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.
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, app, Docker) marked set or missing, and a live test of the database, Firebase, push, purchases, checkout, AI, storage, email, address search and the worker. Values are never shown. - Command line: in
estatly_web_nextjs/runpnpm run doctor(with Docker:docker compose run --rm migrate pnpm run doctor).
9. Run the mobile app
- Copy the settings file and fill it in:
Setcd estatly_mobile_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. - 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
| Task | Where |
|---|---|
| Cities and neighbourhoods | Admin → 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 listings | Admin → 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 owners | Admin → 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. |
| Reports | Admin → Reports lists listings people flagged. Take one down with a reason, or dismiss the reports. |
| New-build projects | Admin → Projects: add a development with its photos, stage and floor plans. |
| Property types and fields | Admin → Types & categories and Custom fields (extra details like parking or HOA fee, and the facilities list). |
| Invite agents | Admin → Agents & owners → Invite agent. They get an email and become an agent when they sign in with that email. |
| Import listings | Admin → Properties → Import CSV (the dialog shows the columns). |
| Banners, articles, push campaigns | Admin → Sliders & banners, Articles, Notifications. |
| Staff and permissions | Admin → 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)
- 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.
- Create a project in RevenueCat, add both apps, and import the products.
- Put the product ids in Admin → Packages (each package's store product ids) and on each boost.
- App keys:
REVENUECAT_APPLE_KEYandREVENUECAT_GOOGLE_KEYinestatly_mobile_flutter/.env(RevenueCat → API keys → public app keys). - Server keys in
deploy/.env:REVENUECAT_SECRET_KEY, andREVENUECAT_WEBHOOK_AUTH— the same value you enter in RevenueCat → Integrations → Webhooks, with the URLhttps://your-domain.com/api/v1/webhooks/revenuecat. Renewals, cancellations and expiries reach the server through this webhook.
On the website: Stripe Checkout
- 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,invoice.paid,customer.subscription.updatedandcustomer.subscription.deleted. Copy the signing secret intoSTRIPE_WEBHOOK_SECRET. - 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.
- 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).
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
- Map tiles: OpenStreetMap's public tiles by default — fine to start, but their usage policy asks heavy users to use a provider. Pick Mapbox in Admin → Settings → Maps & location and set
MAPBOX_TOKEN, or set any provider withMAP_TILE_URL(e.g. MapTiler:https://api.maptiler.com/maps/streets-v2/{z}/{x}/{y}.png?key=YOUR_KEY) andMAP_ATTRIBUTION. The app and the website read it 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). The address search country is in Admin → Settings → Maps & location. - Commute times on a listing are estimates from the straight-line distance, a road factor and average speeds you set in Admin → Settings → Maps & location, so no paid routing API is needed. The app labels them as estimates.
- Hidden addresses: sellers can hide the exact address; the pin is then moved by up to the distance set in the same settings.
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
| Who | Methods |
|---|---|
| App | Google, Apple (iOS), phone number (SMS code), and guest (browse, save and search; sign in to post, chat or book). |
| Website | Google, Apple and phone number. |
| Admin | Email 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
--namereplaces the name everywhere it is shown: under the app icon, page titles, emails and push.--idsets the Android package and the iOS bundle id, moves Android'sMainActivity, and updatesFIREBASE_IOS_BUNDLE_IDin the app's.env.--dryshows what would change first.
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.
18. Logo, icon, colours and fonts
The rename tool does these too:
node tools/rename.mjs --color "#0F766E" --font "Manrope" --icon my-icon.png
- Colour: sets the brand colour with matching light and dark tints in the app (
lib/theme/app_colors.dart) and on the website (src/app/globals.css), and the icon and splash backgrounds. Admin → Settings → General → Primary colour overrides it at run time without a new build. - Icon: a square PNG of at least 1024×1024. Then run, in
estatly_mobile_flutter:dart run flutter_launcher_iconsanddart run flutter_native_splash:create. - Website font: any Google Font name.
- Logo on the website and in emails: Admin → Settings → General → Logo URL.
- App font: the app uses DM Sans from
assets/fonts/(declared inpubspec.yamlasDMSans). Put your font files there, change thefonts:entry, and changefontFamilyinlib/theme/app_theme.dart.
19. Basic edits
| I want to change… | Where |
|---|---|
| Terms, privacy policy and About | Admin → Settings → General → Legal pages (shown in the app and on the website). |
| Currency, area and distance units, time zone | Admin → Settings → General → Region. |
| Listing rules (photos, title length, review) | Admin → Settings → Listings. |
| Mortgage calculator defaults | Admin → Settings → Listings → Mortgage calculator defaults. |
| Home banners | Admin → Sliders & banners (app home slider and website hero, with schedule and city). |
| Help centre questions | Admin → Settings → General → Help centre questions. |
| Support email, phone and office | Admin → Settings → General → Support. |
| Force an app update, maintenance message | Admin → Settings → Sign-in & app versions (minimum versions) and General → Access (maintenance mode). |
| App download links on the website | Admin → Settings → Sign-in & app versions (App Store, Google Play and APK links). |
| Texts inside the app | The screens are in estatly_mobile_flutter/lib/pages/; search for the text and edit it. |
| Sample images | Upload 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.
| Key | Required | Where | What it does |
|---|---|---|---|
| Database | |||
DATABASE_URL | Yes | website | 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 | website, Docker | Public URL of the website/API, e.g. https://homes.example.com |
APP_SECRET | — | website, Docker | Long random string that signs private file links (verification documents) |
NEXT_PUBLIC_APP_NAME | — | website, 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 | — | website, Docker | Browser 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_KEY | Yes | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | — | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | — | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_APP_ID | Yes | website, Docker | Firebase web app config |
| Firebase Admin + push | |||
FIREBASE_PROJECT_ID | Yes | website, Docker | Firebase project id |
FIREBASE_CLIENT_EMAIL | Yes | website, Docker | Service account e-mail |
FIREBASE_PRIVATE_KEY | Yes | website, Docker | Service account private key |
| Payments | |||
REVENUECAT_SECRET_KEY | — | website, Docker | RevenueCat secret API key — real App Store / Google Play purchases (empty = demo) |
REVENUECAT_WEBHOOK_AUTH | — | website, Docker | Authorization header RevenueCat sends to /api/v1/webhooks/revenuecat |
STRIPE_SECRET_KEY | — | website, Docker | Stripe secret key — website checkout (empty = demo) |
STRIPE_WEBHOOK_SECRET | — | website, Docker | Stripe 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, Docker | Google Geocoding for address search (empty = OpenStreetMap) |
MAPBOX_TOKEN | — | website, Docker | Mapbox public token, when Settings → Maps uses Mapbox tiles |
MAP_TILE_URL | — | website, Docker | Any other tile URL template (MapTiler, Stadia…); overrides the setting |
MAP_ATTRIBUTION | — | website, Docker | Credit line your tile provider requires |
| AI description writer | |||
AI_API_KEY | — | website, Docker | Key for any OpenAI-compatible API (empty = built-in sample writer) |
AI_BASE_URL | — | website, Docker | API base URL (default https://api.openai.com/v1; OpenRouter, Groq, Ollama…) |
AI_MODEL | — | website, Docker | Model name (default gpt-4o-mini) |
| Storage | |||
S3_ENDPOINT | — | website, Docker | S3-compatible endpoint (MinIO, R2); empty for AWS |
S3_REGION | — | website, Docker | Bucket region |
S3_BUCKET | — | website, Docker | Bucket for photos, videos and documents (empty = local uploads folder) |
S3_ACCESS_KEY_ID | — | website, Docker | Storage access key |
S3_SECRET_ACCESS_KEY | — | website, Docker | Storage secret key |
UPLOAD_DIR | — | website | Folder for uploads when no bucket is set (default ./uploads) |
SMTP_HOST | — | website, Docker | SMTP server; empty = emails are printed to the log |
SMTP_PORT | — | website, Docker | 587 (STARTTLS) or 465 (TLS) |
SMTP_USER | — | website, Docker | SMTP user |
SMTP_PASS | — | website, Docker | SMTP password |
SMTP_SECURE | — | website, Docker | "true" for port 465 (TLS); empty for 587 |
| Mobile app | |||
API_BASE_URL | Yes | app | Your server; the app calls <url>/api/v1 |
APP_NAME | — | app | App name in the UI |
FIREBASE_PROJECT_ID | Yes | app | Firebase project id |
FIREBASE_MESSAGING_SENDER_ID | Yes | app | Firebase config |
FIREBASE_STORAGE_BUCKET | — | app | Firebase config |
FIREBASE_ANDROID_API_KEY | Yes | app | Firebase Android config |
FIREBASE_ANDROID_APP_ID | Yes | app | Firebase Android config |
FIREBASE_IOS_API_KEY | Yes | app | Firebase iOS config |
FIREBASE_IOS_APP_ID | Yes | app | Firebase iOS config |
FIREBASE_IOS_CLIENT_ID | — | app | Google sign-in on iOS |
FIREBASE_IOS_BUNDLE_ID | — | app | iOS bundle id |
GOOGLE_SERVER_CLIENT_ID | — | app | Web OAuth client id — Google sign-in on Android |
REVENUECAT_APPLE_KEY | — | app | RevenueCat public SDK key for iOS (empty = demo purchases) |
REVENUECAT_GOOGLE_KEY | — | app | RevenueCat public SDK key for Android (empty = demo purchases) |
SUPPORT_EMAIL | — | app | Fallback 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
- Create an upload key:
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload. - Create
estatly_mobile_flutter/android/key.properties: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 app collects name, phone, email, approximate location while searching, photos and documents the user uploads, and messages) and the content rating.
- 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), 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.