Swapsy

A local classifieds marketplace where people buy, sell and swap: one Flutter app (iOS and Android) 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 Swapsy. 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
swapsy_mobile_flutter/The app. People browse ads near them (list and map), search with filters, chat with sellers, make offers or offer a swap, pick a safe meetup spot, save favourites and searches with alerts, and post their own ads with boosts.
swapsy_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 (ads, categories, chats, offers, meetups, reviews, payments) lives in your PostgreSQL database. Photos and ID-check images 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 an ad: a member posts it in the 7-step form → it waits in Admin → Moderation (trusted, ID-verified sellers can skip review) → once approved it shows in search, on the map, in saved-search alerts and to the seller's followers → buyers chat, make an offer or offer a swap, and agree a safe meetup spot → the seller marks it sold, both sides rate each other, or the ad expires after the days set in the admin.

Nothing is blocked on a key. Without payment keys, boosts and packages 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.

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 swapsy-1.0.0.zip root@YOUR_SERVER_IP:, then on the server:
    apt install -y unzip
    unzip swapsy-1.0.0.zip
    cd swapsy/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, cities, safe spots, packages) is loaded once.
  4. Open APP_URL/install in your browser. The install wizard creates your admin account, names the marketplace, sets the currency and units, and can load the sample ads (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, email and guest) 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 (members and admin staff) and Apple (see chapter 15).
  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.swapsy). Rename the app first if you want your own id (chapter 16). Or use the FlutterFire CLI, which registers both for you:
    dart pub global activate flutterfire_cli
    cd swapsy_mobile_flutter
    flutterfire configure --project your-project --platforms android,ios \
      --android-package-name com.yourcompany.swapsy --ios-bundle-id com.yourcompany.swapsy
  8. Copy the app's values into swapsy_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 swapsy_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 swapsy swapsy | gzip > /root/backups/swapsy-$(date +%F).sql.gz

Also back up the minio-data volume (photos and ID-check images), 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 swapsy_web_nextjs/ into Vercel. Add every key from swapsy_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 swapsy_web_nextjs && pnpm install && pnpm prisma:migrate:deploy && pnpm prisma:seed -- --base (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, offers do not expire, reservations are not released, boosts do not start or end, and alerts, meetup reminders and scheduled notifications are not sent.

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; ID-check images are only shown to admin staff.

Without Docker

Install Node.js 22 and pnpm 11, then in swapsy_web_nextjs/: cp .env.example .env, fill it in, and run pnpm install, pnpm prisma:migrate:deploy, pnpm prisma:seed -- --base, pnpm build, then keep pnpm start and pnpm worker running (for example with pm2). Without S3_* keys, uploads are stored in the 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 & staff.
  2. The marketplace: name, currency, distance unit (miles or kilometres) and time zone.
  3. The look and content: the brand colour, and Load the sample ads for sample members, ads, chats, offers and reviews around Austin, Texas, so every screen has content. Without it you start with only the base data: categories with their custom fields, cities and areas, safe meetup spots, packages, report reasons, FAQ and safety tips.

The wizard closes for good once a super admin exists. Sign in later at /admin.

The sample people, ads and texts are fictional, and the sample photos are placeholders. Delete or edit them in the admin when real ones arrive; add your own cities in Admin → Cities & areas.

8. Setup check

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

9. Run the mobile app

  1. Fill in the settings file (the download ships a copy of .env.example as .env so the app builds):
    cd swapsy_mobile_flutter
    nano .env
    Set API_BASE_URL to your server (https://your-domain.com; for pnpm dev on your computer use http://10.0.2.2:3351 in the Android emulator) and the Firebase values from chapter 4.
  2. Get the packages and run:
    flutter pub get
    flutter run

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
Review new adsAdmin → Moderation: open an ad, check the photos, text and the automatic risk flags (pay-outside-the-app wording, phone numbers or links, a price far below similar ads, duplicates, new sellers, reports), then approve or reject with a reason the seller sees. Admin → Settings → Marketplace rules sets the review mode (everything, or trusted sellers go live at once).
Categories and custom fieldsAdmin → Categories (icons, colours, subcategories, whether swaps are allowed, manual review) and Custom fields (the extra details each category asks for, such as brand, storage or mileage; filterable fields appear in search filters).
Cities, areas and safe spotsAdmin → Cities & areas and Meetup spots: add police Safe Exchange Zones, staffed stores, libraries and banks. Chats suggest the spot halfway between the two people.
Verify membersAdmin → Verification: compare the ID photos with the selfie, then approve or reject with a reason. Approved members get the ID-verified badge. ID images are deleted 30 days after the decision.
ReportsAdmin → Reports lists ads, members and chats people reported. Take action (for example remove the ad or suspend the member) or dismiss the report. Admin → Reports → Reasons edits the reasons people can pick.
MembersAdmin → Users: search, filter, export CSV, open a member to see their ads, reports and payments, and suspend or ban.
Banners, blog, FAQ, safety tips, pushAdmin → Banners, Blog, FAQ, Safety tips and Notifications.
Staff and permissionsAdmin → Roles & staff: add staff and choose what each role may view, create, edit and delete. The Audit log shows who changed what.

11. Boosts, packages and payments

Members can keep a number of ads live for free (Admin → Settings → Marketplace rules). Boosts (Bump, Featured, Top of category, and a 5-boost bundle) put one ad higher in search and on Home for a few days and can be scheduled to start later. Listing packages (Starter pack, Pro Seller) raise how many ads can be live. Edit them in Admin → Packages; the server enforces every limit.

Card payments on the website (and in the Android app)

Turn on any of these; buyers pay on the provider's own secure page and come back to /pay/return. Set the keys in deploy/.env, then switch each gateway on in Admin → Settings → Payment gateways.

GatewayKeysWebhook URL (in the provider's dashboard)
Stripe (cards, Apple Pay, Google Pay)STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECREThttps://your-domain.com/api/v1/webhooks/stripe with the events checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired
PayPalPAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_MODE=liveNone needed: the payment is confirmed when the buyer returns.
Razorpay (India)RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECREThttps://your-domain.com/api/v1/webhooks/razorpay, event payment_link.paid
Flutterwave (Africa)FLUTTERWAVE_SECRET_KEY, FLUTTERWAVE_WEBHOOK_HASHhttps://your-domain.com/api/v1/webhooks/flutterwave (set the same secret hash there)

In the iPhone app: RevenueCat

Apple requires in-app purchases for digital goods in iOS apps. On iPhone, boosts and packages are bought through the App Store with RevenueCat; on Android the app opens the same secure web checkout.

  1. Create one consumable product per boost and package in App Store Connect (and in the Play Console if you prefer store billing on Android too).
  2. Create a project in RevenueCat, add the apps and import the products.
  3. Put each product id on its package in Admin → Packages (iOS product id, Android product id).
  4. App keys: REVENUECAT_IOS_KEY and REVENUECAT_ANDROID_KEY in swapsy_mobile_flutter/.env (RevenueCat → API keys → public app keys).
  5. Server keys in deploy/.env: REVENUECAT_API_KEY (secret key, used to confirm each purchase) and REVENUECAT_WEBHOOK_SECRET, entered in RevenueCat → Integrations → Webhooks as the Authorization header Bearer YOUR_SECRET with the URL https://your-domain.com/api/v1/webhooks/revenuecat. Then turn on in-app purchases in Admin → Settings → Payment gateways.

Demo purchases

While no gateway is set, purchases are granted at once with a DEMO label. Set DEMO_PAYMENTS=false to switch that off before your keys are in. Sales tax and its label are in Admin → Settings → Payment gateways.

Charging members through your product requires the Envato Extended Licence.

12. Push notifications

New messages, offers and counter-offers, meetup updates, ad decisions, price drops, saved-search alerts, new ads from followed sellers 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.

Every member also has a notification inbox (app and website) and switches for each kind of notification in their settings.

13. Maps, cities and safe spots

14. 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 are in Admin → Settings → Email, and every email's text is in Admin → Email templates. Until SMTP is set, emails are printed to the server log (docker compose logs web).

15. Sign-in methods

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

Each method must be on in Firebase (chapter 4). Guest mode can be switched off in Admin → Settings → General.

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.

16. Rename the app and package id

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

node tools/rename.mjs --name "TradeHub" --id com.yourcompany.tradehub

Then register the new id in Firebase (chapter 4). The name people see 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 "#7C3AED" --font "Manrope" --icon my-icon.png

18. 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, distance unitAdmin → Settings → Currency & units.
Free live ads, ad lifetime, renewals, photos per ad, offer expiry, counters, reservation time, swaps on/offAdmin → Settings → Marketplace rules.
Home bannersAdmin → Banners (app home slider and website, with a schedule).
Help centre questions, safety tipsAdmin → FAQ and Safety tips.
Support email and phoneAdmin → Settings → General.
Force an app update, maintenance messageAdmin → Settings → General (minimum and latest app version) and Maintenance.
App download links on the websiteAdmin → Settings → General (App Store, Google Play and APK links).
Texts inside the appThe screens are in swapsy_mobile_flutter/lib/pages/; search for the text and edit it.
Texts on the websiteThe pages are in swapsy_web_nextjs/src/app/(site)/.

19. Every key, explained

Each folder has a .env.example that lists its keys with comments. This table lists all of them: website = swapsy_web_nextjs/.env, Docker = deploy/.env, app = swapsy_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://swapsy.example.com
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
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
STRIPE_SECRET_KEY—website, DockerStripe secret key — cards, Apple Pay, Google Pay on web and Android
STRIPE_WEBHOOK_SECRET—website, DockerStripe webhook signing secret (/api/v1/webhooks/stripe)
PAYPAL_CLIENT_ID—website, DockerPayPal REST app client id
PAYPAL_CLIENT_SECRET—website, DockerPayPal REST app secret
PAYPAL_MODE—website, Docker"live" for real payments; empty = sandbox
RAZORPAY_KEY_ID—website, DockerRazorpay key id (India)
RAZORPAY_KEY_SECRET—website, DockerRazorpay key secret
RAZORPAY_WEBHOOK_SECRET—website, DockerRazorpay webhook secret (/api/v1/webhooks/razorpay)
FLUTTERWAVE_SECRET_KEY—website, DockerFlutterwave secret key (Africa)
FLUTTERWAVE_WEBHOOK_HASH—website, DockerFlutterwave webhook secret hash
REVENUECAT_API_KEY—website, DockerRevenueCat secret API key — confirms in-app purchases
REVENUECAT_WEBHOOK_SECRET—website, DockerRevenueCat webhook Authorization value
DEMO_PAYMENTS—website, Docker"false" turns off the free DEMO purchases while no gateway is set
Maps
GOOGLE_MAPS_API_KEY—website, DockerGoogle Geocoding for address search (empty = OpenStreetMap)
MAP_TILE_URL—website, DockerMap tiles URL template (MapTiler, Stadia, Mapbox…)
MAP_ATTRIBUTION—website, DockerCredit line your tile provider requires
Storage
S3_ENDPOINT—website, DockerS3-compatible endpoint (MinIO, R2); empty for AWS
S3_REGION—website, DockerBucket region
S3_BUCKET—website, DockerBucket for photos and documents (empty = local uploads folder)
S3_ACCESS_KEY_ID—website, DockerStorage access key
S3_SECRET_ACCESS_KEY—website, DockerStorage secret key
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
Storage
UPLOAD_DIR—websiteFolder for uploads when no bucket is set (default ./uploads)
Firebase Admin + push
FCM_TOPIC_PREFIX—website, DockerPrefix of the push topics (default swapsy)
Email
EMAIL_DISABLED—website, Docker"true" logs emails instead of sending (public demos)
Mobile apps
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
SUPPORT_EMAIL—appFallback support e-mail until the server config loads
REVENUECAT_IOS_KEY—appRevenueCat public iOS SDK key (empty = boosts paid on the web checkout or DEMO)
REVENUECAT_ANDROID_KEY—appRevenueCat public Android SDK key (empty = web checkout or DEMO)
DEMO_SIGN_IN—app"true" shows one-tap demo sign-in (your public demo only)

20. File structure

The folders below build these screens: the app (swapsy_mobile_flutter) and the website and admin (swapsy_web_nextjs).

App: Home
App · Home (pages/home/)
App: chat with an offer card
App · Chat with an offer (pages/chat/)
Website: landing page
Website · Landing page (src/app/(site)/page.tsx)

Mobile app (swapsy_mobile_flutter/lib)

main.dart            starts Firebase and the app
app.dart             theme, push and deep links
router.dart          every screen's route and the start-up gates (setup → onboarding → location → sign-in)
routes.dart          route names
pages/               one folder per area: launch, auth, home, search, ad, seller, chat, meetup, post
                     (the 7-step form), my_ads, boost, favourites, notifications, help, profile
widgets/             shared app widgets: ad cards, the ads map, sign-in sheet
providers/           app state with Riverpod (session, favourites, data from the API)
core/api/            API client
core/auth/           sign-in (Google, Apple, phone, email, guest)
core/config/         .env reading and Firebase options
core/models/         Ad, Category, Chat, Offer, Meetup, Config…
core/theme/          colour tokens, text styles, light and dark themes
core/widgets/        building blocks (buttons, cards, sheets, empty and error states, skeletons)
core/utils/          formatting and helpers

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

Server (swapsy_web_nextjs/src)

app/(site)/          the public website (home, search, ad pages, sellers, post, account, chats, pricing,
                     checkout, blog, help, safety, spots, legal)
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), ads, chat (offers, swaps, meetups),
                     money and payments, notify (push), email, storage, geo, settings, setup-check
lib/client/          browser code: Firebase sign-in, API calls
components/          site/ (website), panel/ (admin), app/ (shared), ui/ (building blocks)
database/            Prisma schema, migrations, seed (base and 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).

21. Publish to the stores

You publish the app under your own developer accounts. Rename it first (chapter 16).

Google Play

  1. Create an upload key: keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload.
  2. Create swapsy_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, photos the user uploads, ID-check images, 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 an ad and chatting need an account.

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

23. FAQ and troubleshooting

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:PORT. 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 ad does not show in search

It waits for review in Admin → Moderation (or set the review mode to trusted sellers in Admin → Settings → Marketplace rules).

Purchases say "DEMO"

No payment 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).

Offers never expire, boosts never start, alerts 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 (a hot reload does not re-read .env).

Where are the logs?

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

24. Credits

Swapsy 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, 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, 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, MinIO, Caddy.

Fonts, maps and content

Poppins (SIL Open Font License 1.1, licence file in swapsy_mobile_flutter/assets/fonts/OFL.txt). Map data © OpenStreetMap contributors. The sample ads, people, reviews and texts are original and fictional. The sample photos are labelled placeholders; the photos on the live demo are not part of the download.

25. Changelog

1.0.0 — 2026-10-07

First release. The full list is in CHANGELOG.md.

Not in this version

App languages other than English, two-step sign-in for admins, automatic face matching for ID checks (staff compare the photos by eye), and drag-to-reorder banners.

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