Harmonia Documentation

Welcome to the official documentation for Harmonia, a music streaming platform made of three projects: a Flutter mobile app for Android and iOS, a NestJS backend API, and a Next.js admin panel. Follow this guide to set everything up, brand it and publish it.

What is in this package: Setting up for the first time? Start with the backend (section 13), then the admin panel (section 14), then come back to section 1 for the app. The app and the admin panel both need a running backend.
Contents
  1. Feature Walkthrough
  2. Third-Party Services and Costs
  3. How the Pieces Connect (Architecture)
  4. 1. Prerequisites
  5. 2. Environment Setup
  6. 3. Change Package Name and Bundle ID
  7. 4. Firebase Setup
  8. 5. Social Sign-In and Push Notifications
  9. 6. Change App Name
  10. 7. App Icon, Splash and Colours
  11. 8. Connect the App to Your Backend
  12. 9. Languages
  13. 10. AdMob
  14. 11. Premium Subscriptions (Google Play)
  15. 12. Build and Release
  16. 13. Backend Setup (NestJS)
  17. 14. Admin Panel Setup (Next.js)
  18. 15. API Reference
  19. 16. Database Schema
  20. 17. FAQ and Common Build Errors
  21. 18. Support
  22. 19. Changelog
  23. 20. Upgrading to a New Version

Feature Walkthrough

A plain-language tour of what the app does, so you can find your way around before touching any code. Nothing in this section needs programming knowledge — the technical setup starts at section 1.

First launch

The four main tabs

TabWhat the listener does there
HomeBanner carousel, then the sections you arrange in the admin panel: top hits, playlists, just arrived, artists, moods, genres, shorts, recently played and continue listening. Free listeners also see a banner ad.
ExploreSearch by text or voice, with trending searches and suggestions. Browse all, moods, genres and fresh picks.
PremiumPremium plans with store prices, the free trial offer, the current plan status, and Restore purchases.
LibraryPlaylists, saved albums and artists, liked songs, history and downloads, with filters, search, and a grid or list view. Listeners create their own playlists here.

Listening

Free and Premium

You decide which songs, albums and playlists are Premium. Free listeners see Premium content locked and hear ads; Premium listeners get ad-free listening, downloads and Premium-only content. Premium is sold as a subscription through Google Play Billing (see section 11).

Settings

Profile and photo, your devices (sign any device out remotely), music taste, manage subscription, playback options, audio quality, push notifications, theme (light, dark or system), language (English or Bangla), clear cache, legal pages, credits, sign out and delete account.

What you control from the admin panel

Almost everything listeners see is managed in the browser, without rebuilding the app: songs, albums, artists, playlists, shorts, home sections, banners, moods and genres, Premium plans and the Premium screen text, ads, push notifications, app version and maintenance mode, legal links, app users and staff accounts. The full list is in section 14.

Recommended first hour. Set up the backend (section 13), start the admin panel (section 14) and sign in with your admin account. Upload one artist, one song with an audio file and cover, and add it to a home section. Then point the app at your backend (section 8), connect Firebase (section 4) and run it — your song should appear on the Home screen. Once that round trip works, move on to branding and store setup.

Third-Party Services and Costs

Harmonia is self-hosted: you receive the full source code, with no licence keys and nothing payable to us. Some features connect to outside services that are not included in the purchase.

Buyers are responsible for all fees charged by third-party services. This includes hosting, developer accounts, app store commissions, storage and any usage beyond a provider's free tier. The item price covers the source code and documentation only. Prices change; check each provider's current rates.
Accounts you must create yourself. Harmonia ships no credentials of ours. Before the app works you need:
ServiceRequired?Used forWho bills you
VPS or cloud serverRequiredRuns the backend (Node.js 20+, PostgreSQL 14+) and the admin panelYour hosting provider
Firebase (your own project)RequiredApp sign-in (Authentication) and push notifications (Cloud Messaging). The free Spark plan covers typical use. Subject to Google's Firebase termsGoogle, beyond the free tier
Google Play Developer API (your own service account)Required for PremiumThe backend checks every Google Play purchase with GoogleFree API; Google Cloud project needed
Cloudflare R2RequiredStores audio, covers and images uploaded from the admin panelCloudflare, beyond the free tier
Google Play Developer accountRequired for AndroidPublishing the app; USD 25 one-timeGoogle
Apple Developer ProgramRequired for iOSPublishing the app and Sign in with Apple; USD 99 per yearApple
Google Play BillingOptionalPremium subscriptions. The only payment method included. Google takes a commission on each sale, typically 15–30%Google (deducted from sales)
Google Cloud Pub/SubOptionalReal-time notifications about renewals and refunds from Google PlayGoogle, beyond the free tier
Apple In-App Purchase, Stripe, PayPal, other gatewaysNot includedNo checkout integration ships for these. Premium cannot be bought on iOS in this version—
Google AdMob (your own account)OptionalBanner and interstitial ads for free listeners. You must follow AdMob's policies (section 10)Free to join; you are responsible for policy compliance and any costs related to your account or traffic
Meta for Developers (your own Facebook App ID)OptionalFacebook sign-inFree
Cloudflare WorkersOptionalExpiring audio links (the included worker script)Cloudflare, beyond the free tier
Music licensingRequired for real contentYou must own or license every track you publish. No music is included in the packageRights holders / licensing bodies

How the Pieces Connect (Architecture)

Harmonia is three projects that talk to one backend. Only the backend talks to the database and holds secrets; the app and the admin panel never connect to PostgreSQL or R2 credentials directly.

Flutter App Android and iOS Flutter-App/ Admin Panel Next.js, in the browser Admin-Panel/ Backend API NestJS, https://api.your-domain.com/api/v1 Backend/ (run with PM2) auth checks, Premium rules, media processing PostgreSQL all catalogue, user and billing data Cloudflare R2 audio, covers, avatars Firebase Authentication (sign-in) Cloud Messaging (push) Google Play / AdMob subscriptions and ads REST + Firebase ID token REST + admin JWT sign in verify, push push TypeORM upload files stream audio (public / signed URL) buy, ads verify purchase, RTDN (Pub/Sub)
ConnectionWhat happens
App → FirebaseThe listener signs in (email, Google, Facebook or Apple). Firebase returns an ID token.
App → BackendEvery request goes to https://your-api/api/v1/... with the Firebase ID token in Authorization: Bearer (guests send none). The backend verifies the token with the Firebase Admin SDK.
Admin panel → BackendStaff sign in with email and password; the backend returns a JWT that the panel sends on every /admin/... request. Roles: viewer, editor, super admin.
Backend → PostgreSQLAll data lives here. Only the backend connects.
Backend → R2, App → R2The admin uploads files through the backend, which stores them in R2 and makes the low/normal/high audio versions. The app streams audio and images straight from the R2 public URL (or an expiring signed URL if you enable it).
Backend → Firebase Cloud MessagingPush notifications composed in the admin panel are sent by the backend, to the broadcast topic or to registered devices.
App ↔ Google Play → BackendThe app buys a subscription with Google Play Billing and sends the purchase token to the backend, which checks it with Google. Google then reports renewals and cancellations to the backend through real-time developer notifications (Pub/Sub).
App → AdMobFree listeners see AdMob ads. The ad unit IDs come from the backend (GET /ads/config), set on the admin panel's Ads page.

1. Prerequisites

Use these versions for the smoothest build. The app was built and tested with the versions marked "tested".

Flutter SDKTested: 3.47.6
Min: 3.44.0
Java (JDK)17 or 21
(Android Studio's bundled JDK works)
XcodeTested: 26
Min: 16
CocoaPodsLatest (1.16 or newer)
Node.js20 LTS or newer
PostgreSQL14 or newer
FFmpegAny recent version
(on the server)
IDEAndroid Studio / VS Code

Server size

The backend, PostgreSQL and the admin panel can run together on one Linux server (Ubuntu 22.04 or 24.04 LTS recommended). Audio and images are stored in Cloudflare R2, not on the server, so the disk only needs room for the code, the database and temporary files during uploads.

CPURAMDiskGood for
Minimum1 vCPU2 GB (plus 2 GB swap)20 GB SSDTesting and a small catalogue; uploads convert slowly
Recommended2 vCPU4 GB40 GB SSDLaunch, thousands of listeners, regular uploads
Growing4 vCPU or more8 GB or more80 GB SSDLarge catalogue and many daily listeners; consider a managed PostgreSQL

CPU matters most when you upload songs, because FFmpeg converts each one into three stream qualities. Building the admin panel (npm run build) needs about 1.5 GB of free RAM; on a 2 GB server add swap first. A shared hosting plan (cPanel without SSH and Node.js) is not enough; you need a VPS or cloud server with SSH access.

Java version: Gradle 8.14, used by this project, runs on JDK 17 to 24. Newer JDKs fail with "Unsupported class file major version". If java -version shows something newer, point Flutter at Android Studio's JDK: flutter config --jdk-dir "<Android Studio>/jbr".
Need help? If you hit a version problem, check the FAQ or contact us through the Support section.

2. Environment Setup

Select your operating system to see the setup steps.

Android Studio

  • Download Android Studio from the official page, run the installer and let it download the SDK components.
  • Optional: create an emulator in the Device Manager.

Flutter SDK

  • Download the stable Windows release from the Flutter download page and extract it, for example to C:\flutter.
  • Add C:\flutter\bin to the Path environment variable (This PC > Properties > Advanced system settings > Environment Variables).
  • Open a new Command Prompt and run flutter doctor. Fix anything it reports.

IDE

  • In Android Studio open File > Settings > Plugins > Marketplace, install the Flutter and Dart plugins and restart.
▶ Watch Windows Setup Video

Android Studio

  • Download Android Studio from the official page, drag it into Applications and complete the setup wizard.

Flutter SDK

  • Download the stable macOS release from the Flutter download page and extract it: tar xf flutter_macos_<version>.tar.xz
  • Add flutter/bin to your PATH in ~/.zshrc, then open a new terminal and run flutter doctor.

Xcode and CocoaPods (for iOS)

  • Install Xcode from the Mac App Store, open it once to finish installing components, then run sudo xcodebuild -runFirstLaunch.
  • Install CocoaPods: sudo gem install cocoapods (or brew install cocoapods).

IDE

  • In Android Studio open Settings > Plugins > Marketplace, install Flutter and Dart and restart.
▶ Watch macOS Setup Video

Android Studio

  • Download Android Studio from the official page, extract it (tar -xvzf android-studio-*-linux.tar.gz) and run bin/studio.sh.

Flutter SDK

  • Download the stable Linux release from the Flutter download page and extract it: tar xf flutter_linux_<version>.tar.xz
  • Add flutter/bin to your PATH, then run flutter doctor.

IDE

  • In Android Studio open File > Settings > Plugins > Marketplace, install Flutter and Dart and restart.
▶ Watch Linux Setup Video

Open and run the app

cd Flutter-App
flutter pub get
flutter run

Generated code (models and translations) is already included. Regenerate it only after you change models or translation files:

dart run build_runner build --delete-conflicting-outputs
flutter gen-l10n

3. Change Package Name and Bundle ID

Every app needs a unique identifier, for example com.yourcompany.music. Change it before you set up Firebase, because Firebase apps are registered by this identifier.

Android

FileChange
android/app/build.gradle.ktsnamespace and applicationId (currently com.harmonia.music.player)
android/app/src/main/AndroidManifest.xmlthe package attribute on <manifest>
android/app/src/main/kotlin/.../MainActivity.kt
.../NowPlayingWidgetProvider.kt
the package line at the top of both files. You may also move both files into a folder that matches the new package (optional; the build works either way).

iOS

Tip: the Dart package name in pubspec.yaml (harmonia) is internal and never shown to users. You can leave it as is; renaming it means updating every package:harmonia/ import.

4. Firebase Setup

Firebase provides sign-in and push notifications.

You must use your own Firebase project. The package ships placeholder Firebase files with values such as YOUR_ANDROID_API_KEY and your-firebase-project-id. They let the project compile, but sign-in and push notifications will not work until you replace them: No Firebase project of ours is included, so your app never connects to our accounts. Steps 1 and 2 below replace all three files.

Step 1: Create a Firebase project

Step 2: Connect the app (recommended: FlutterFire CLI)

The FlutterFire CLI registers your Android and iOS apps and writes all config files for you.

npm install -g firebase-tools
firebase login
dart pub global activate flutterfire_cli
cd Flutter-App
flutterfire configure --project=<your-firebase-project-id>

Select android and ios, and confirm the package name and bundle ID from section 3. The command updates:

Manual alternative

Step 3: Enable sign-in methods

Step 4: Service account for the backend

The backend verifies sign-ins and sends push notifications with a Firebase service account: Project settings > Service accounts > Generate new private key. You will paste this JSON into the backend's .env in section 13.

▶ Firebase + Flutter video tutorials

5. Social Sign-In and Push Notifications

The login screen shows Google, Facebook and Apple buttons on both platforms. Configure the ones you want; to hide a button, remove it from lib/feature/auth/presentation/widgets/social_media_buttons.dart.

Google

Facebook

Register your own Facebook app. Meta's Platform Policy requires every app to use its own App ID. The package contains no Facebook App ID or client token, only the placeholders YOUR_FACEBOOK_APP_ID and YOUR_FACEBOOK_CLIENT_TOKEN. Facebook sign-in stays disabled until you replace them with your own values. If you do not want Facebook sign-in, remove the button (see the top of this section).
Your Meta account, your responsibility. Facebook Login runs through your own app on the Meta for Developers platform. You are responsible for any costs and for following Meta's Platform Terms and Developer Policies, including app review, business verification, the privacy policy URL and the data deletion instructions Meta requires. The author of this item is not responsible for your Meta app or its approval.

Apple

Push notifications

6. Change App Name

WhereFileKey
Android launcher nameandroid/app/src/main/AndroidManifest.xmlandroid:label
Android widget and app stringsandroid/app/src/main/res/values/strings.xmlapp_name, app_display_name
iOS home-screen nameios/Runner/Info.plistCFBundleDisplayName, CFBundleName
Name inside the applib/l10n/app_en.arb, lib/l10n/app_bn.arbappName (then run flutter gen-l10n)
Splash wordmark and taglinelib/feature/splash/presentation/splash_screen.dart, ARB key splashTagline'HARMONIA'
Other on-screen brand textlib/feature/auth/presentation/pages/login_page.dart, home_app_bar.dart, lyric_card_page.dart, local_notification_service.dartsearch for Harmonia

Tip: search the lib/ folder for Harmonia to find every occurrence.

7. App Icon, Splash and Colours

App icon

Splash screen

Brand colours

The brand colours are in lib/core/theme/src/theme_extension/src/colors/semantic.dart. Flutter writes colours as 0xAARRGGBB: FF (fully opaque) followed by the normal hex code. The default brand green is #1DDA63:

// lib/core/theme/src/theme_extension/src/colors/semantic.dart
static const Color primary = Color(0xFF1DDA63);        // brand green #1DDA63
static const Color darkBackground = Color(0xFF020E28); // dark theme page #020E28
static const Color darkSurface = Color(0xFF0B1B3D);    // dark cards #0B1B3D

Example: to use orange #FF6B00 as your brand colour, change the first line to Color(0xFFFF6B00), then stop the app and run it again (hot reload does not update const colours).

8. Connect the App to Your Backend

Open lib/core/network/constants/api_endpoint.dart and set your backend's public address:

static const String apiHost = 'https://your-domain.com';   // change to e.g. https://api.yourdomain.com

Privacy policy, terms, support e-mail, store links, minimum app version and maintenance mode are not in the app code. They come from the backend and are edited on the admin panel's App config page.

A privacy policy template that matches the app's features is included at Flutter-App/docs/privacy-policy.md. Replace the [PLACEHOLDERS] with your details, remove the parts for features you turned off, have it checked for your countries, publish it at a public URL and paste that URL into App config. Google Play, the App Store and Meta all require one.

9. Languages

The app ships with English and Bangla. By default it follows the phone's language when it is supported, otherwise English. Listeners can choose in Settings > Appearance > Language.

What a language file looks like

Each language is one JSON file of "key": "text" pairs. The keys are the same in every file; only the text changes. Entries starting with @ describe placeholders and exist only in the English template. Here is part of the existing English file and its Bangla counterpart:

// lib/l10n/app_en.arb  (template)
{
  "@@locale": "en",
  "appName": "Harmonia",
  "settingsTitle": "Settings",
  "settingsLanguage": "Language",
  "settingsClearCacheSubtitle": "{size} used by images and temporary files",
  "@settingsClearCacheSubtitle": {
    "placeholders": { "size": { "type": "String" } }
  }
}

// lib/l10n/app_bn.arb  (existing Bangla)
{
  "@@locale": "bn",
  "appName": "হারমোনিয়া",
  "settingsTitle": "সেটিংস",
  "settingsLanguage": "ভাষা",
  "settingsClearCacheSubtitle": "ছবি আর অস্থায়ী ফাইলে {size} জায়গা নিচ্ছে"
}

A new Spanish file lib/l10n/app_es.arb should contain the same keys, "@@locale": "es", and keep every {placeholder} exactly as in English:

// lib/l10n/app_es.arb  (new)
{
  "@@locale": "es",
  "appName": "Harmonia",
  "settingsTitle": "Ajustes",
  "settingsLanguage": "Idioma",
  "settingsClearCacheSubtitle": "{size} usados por imágenes y archivos temporales"
}

Check: flutter gen-l10n finishes without errors and lists no untranslated keys for es. Any key you leave out falls back to English.

Add a language

Remove a language

Delete its ARB file, remove it from supportedLanguages and the language tile, then run flutter gen-l10n.

Catalogue content (song titles, playlist names and so on) is shown as entered in the admin panel.

10. AdMob

The package contains only Google's official AdMob test IDs (publisher ca-app-pub-3940256099942544, listed in Google's test ads guide). Ads show as "Test Ad" and earn nothing. No publisher ID of ours is included anywhere.

Replace every test ID with your own before you publish, or you will earn no ad revenue. Create an app and ad units in your AdMob account first.
Default test valueWhereReplace with
ca-app-pub-3940256099942544~3347511713Android app ID, android/app/build.gradle.ktsyour Android AdMob app ID
ca-app-pub-3940256099942544~1458002511iOS app ID, ios/Runner/Info.plistyour iOS AdMob app ID
ca-app-pub-3940256099942544/6300978111Android banner unit, admin panel Ads pageyour Android banner unit ID
ca-app-pub-3940256099942544/1033173712Android interstitial unit, admin panel Ads pageyour Android interstitial unit ID
ca-app-pub-3940256099942544/2934735716iOS banner unit, admin panel Ads pageyour iOS banner unit ID
ca-app-pub-3940256099942544/4411468910iOS interstitial unit, admin panel Ads pageyour iOS interstitial unit ID

The four ad unit test IDs are written to the database the first time the backend starts (Backend/src/modules/subscriptions/premium.defaults.ts). After that, change them on the admin panel's Ads page; editing the defaults file later has no effect on an existing database.

App IDs (in the app)

PlatformFileKey
Androidandroid/app/build.gradle.ktsmanifestPlaceholders["admobAppId"] in the release block (keep the test ID in the debug block)
iOSios/Runner/Info.plistGADApplicationIdentifier

Ad unit IDs (in the admin panel)

Banner and interstitial unit IDs are not in the app. Set them on the admin panel's Ads page, separately for Android and iOS, together with how often interstitials appear (every N songs, minimum interval). The app reads them from GET /api/v1/ads/config at launch, so you can change them without a new release.

iOS: if you serve ads on iOS, also add Google's SKAdNetworkItems list and, if you use personalised ads, an App Tracking Transparency prompt (NSUserTrackingUsageDescription). See Google's AdMob Flutter quick start.

Premium listeners never see ads.

Your AdMob account, your responsibility. AdMob is free to join, but you are responsible for following the AdMob program policies and Google Publisher Policies (for example no clicks on your own ads, no ads on content you have no rights to, correct ad placement, and privacy consent such as GDPR/UMP and App Tracking Transparency where required). You are also responsible for any costs connected to your account or traffic, such as a paid consent-management platform, mediation partners, invalid-traffic deductions or advertising you buy to grow the app. Policy violations can limit or close your AdMob account; we cannot help with account appeals.

11. Premium Subscriptions (Google Play)

Platform note: Premium is sold through Google Play Billing on Android. Apple In-App Purchase verification is not included in this version, so iOS purchases are switched off by default in three places: the app never starts an App Store purchase or restore, the backend sends no plans to iOS, and the admin panel only allows Android / Google Play plans. iOS listeners use the free tier; Premium can still be granted manually from the admin panel on any platform.
Premium is hidden on iOS by default. Because Premium cannot be bought on iOS in this version, the iOS app does not show anything Premium to free users: Listeners who already have Premium (bought on Android or granted in the admin panel) still see and play their Premium content on iOS. Android is not affected. Everything is controlled by one switch, PremiumStoreSupport.appStorePurchasesEnabled in Flutter-App/lib/feature/premium/domain/entities/premium_entities.dart: when you add Apple In-App Purchase (approach C below) and set it to true, the Premium tab and locked content come back on iOS.

iOS and App Store review

Apple requires digital subscriptions sold inside an iOS app to use Apple In-App Purchase (App Review Guideline 3.1.1). Harmonia's backend verifies Google Play purchases only, so on iOS the app must not offer a way to buy Premium. Choose one approach before you submit to the App Store:

ApproachWhat to do
A. Free on iOS (recommended, no coding)
  • Nothing to change: this is the default. Plans are Android / Google Play only, the backend returns no plans for GET /premium/config?platform=ios, and the iOS app hides the Premium tab, locked content and every "Go Premium" prompt (see the box above).
  • Never mention prices, "subscribe on our website" or "buy on Android" in the iOS app. Apple rejects that under 3.1.1 and 3.1.3.
B. Give the reviewer PremiumCreate a review account, grant it Premium in the admin panel (App users > open the user > Grant days), and enter its email and password in App Store Connect > App Review Information. Premium granted by you is not a purchase, so this is allowed when the iOS app has no purchase buttons (approach A).
C. Add Apple In-App PurchaseDeveloper work: create subscriptions in App Store Connect, add their IDs to the plans (the database already has apple_product_id), and add App Store receipt / transaction verification and App Store Server Notifications to the backend, next to the Google Play code in Backend/src/modules/subscriptions/. Only after verification works, turn on the three switches:
  • Backend: APP_STORE_PURCHASES_ENABLED = true in Backend/src/modules/subscriptions/store-support.ts
  • App: appStorePurchasesEnabled = true in Flutter-App/lib/feature/premium/domain/entities/premium_entities.dart
  • Admin panel: APP_STORE_PURCHASES_ENABLED = true in Admin-Panel/app/(admin)/premium/page.tsx
Turning them on without verification lets Apple charge listeners who then never receive Premium. The app's purchase code already supports StoreKit through the in_app_purchase package.

Suggested App Review notes for approach A and B:

Harmonia is free to use on iOS. The iOS app has no in-app purchases and no
links or references to buying anything elsewhere. Some content is
unlocked for accounts that an administrator grants access to; the demo
account below has this access so you can review every feature.
Demo account: review@your-domain.com / your-password

If App Review still asks for a purchase mechanism, reply in Resolution Center: the iOS app does not sell digital content, access is granted by the service operator, and there are no purchase calls to action in the app. Then either remove the remaining Premium wording on iOS or implement approach C.

Step 1: Create the subscription in Play Console

Step 2: Add the plan in the admin panel

Open Premium plans and create a plan. Fill in the platforms (android), payment provider (google_play), the product ID, base plan ID and optional offer ID — they must match Play Console exactly. Name, features, trial text, tag, colour and button text control how the plan looks in the app. The price shown to listeners always comes from Google Play.

The texts on the Premium screen (title, benefits, trial offer) are edited on the same page under Premium page settings.

Step 3: Let the backend verify purchases

The app sends every purchase to the backend, which confirms it with Google before unlocking Premium. Set up the Google Play service account and real-time notifications as described in section 13, step 7.

How Premium works

12. Build and Release

Version number

Android reads versionCode and versionName from android/app/build.gradle.kts (these override pubspec.yaml). Increase versionCode for every upload. iOS uses version in pubspec.yaml.

Android: create an upload keystore

macOS / Linux:

keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload

Windows:

keytool -genkey -v -keystore %userprofile%\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
Keep the keystore and passwords safe and private. If you lose them you cannot update your app on Google Play. Never commit them to a public repository.

Android: key.properties

Create android/key.properties:

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=upload
storeFile=<full path to upload-keystore.jks>

android/app/build.gradle.kts already reads this file in its signingConfigs block. If key.properties is missing, release builds are signed with the debug key so they still build — Google Play will not accept those.

Android: build

flutter build apk --release          # APK for direct install / testing
flutter build appbundle --release    # App Bundle for Google Play

The App Bundle is created at build/app/outputs/bundle/release/app-release.aab. Upload it in Play Console.

iOS: build

Test on a real device

13. Backend Setup (NestJS)

The backend lives in Backend/. It is a NestJS 11 + TypeORM API on PostgreSQL. The app and the admin panel talk only to this API.

Step 1: Install

cd Backend
npm ci

Install FFmpeg on the server

FFmpeg creates the low, normal and high stream qualities and measures loudness. It is not bundled with the package; install it with your system's package manager (free):

sudo apt update && sudo apt install -y ffmpeg     # Ubuntu / Debian
brew install ffmpeg                                # macOS (local testing)
ffmpeg -version                                    # check it works

The backend finds ffmpeg on the system PATH. If it is installed somewhere else, set its full path in .env: FFMPEG_PATH=/usr/local/bin/ffmpeg. Without FFmpeg the backend still runs and plays the original uploaded files; it just skips the extra stream qualities and loudness analysis and logs a warning.

Step 2: Create the database

createdb harmonia        # or create it in pgAdmin / your host's panel

Step 3: Configure .env

cp .env.example .env

Every variable is explained in .env.example. The important ones:

VariableWhat to set
NODE_ENVdevelopment on your computer, production on the server
PORTAPI port, default 7001
DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAMEYour PostgreSQL connection
JWT_SECRETSigns admin logins. At least 32 characters: openssl rand -hex 48
ADMIN_EMAIL, ADMIN_PASSWORDYour first super admin (see step 4)
FIREBASE_SERVICE_ACCOUNT_JSONThe Firebase service-account JSON from section 4, on one line or base64-encoded
CORS_ORIGINSYour admin panel address, e.g. https://admin.yourdomain.com
CLOUDFLARE_R2_*Media storage (step 6)
GOOGLE_PLAY_*Premium verification (step 7)
MEDIA_URL_SIGNING_SECRETOptional expiring audio links (step 8)
FCM_BROADCAST_TOPICPush topic for "Everyone" notifications, default harmonia.
Must be exactly the same as broadcastTopic in the Flutter app (Flutter-App/lib/core/service/push_notification/push_notification_service.dart). If the two differ, "Everyone" notifications are sent but no phone receives them. See section 5.
TRUST_PROXY1 behind nginx or Cloudflare, so rate limits see real client IPs

To encode the Firebase JSON as base64 on macOS/Linux: base64 -i service-account.json | tr -d '\n'

Push notifications depend on two matching values. FCM_BROADCAST_TOPIC in this .env and broadcastTopic in the Flutter app must be the same text. Both default to harmonia; if you change one, change the other and rebuild the app.
Production safety check. With NODE_ENV=production the server refuses to start while unsafe values remain, and tells you which variable to change: a short or example JWT_SECRET, ALLOW_DEV_USER_HEADER=true, a default ADMIN_PASSWORD, an ADMIN_EMAIL at your-domain.com, a missing Firebase service account, an empty CORS_ORIGINS, or a missing GOOGLE_PLAY_RTDN_TOKEN when Google Play is configured.

Step 4: First run and your admin account

npm run start:dev        # development, with auto-reload; API at http://localhost:7001/api/v1

What a successful first run looks like

This is the real output of npm run start:dev on an empty database, with only the database, JWT_SECRET and admin account set in .env (Firebase, R2 and Google Play not configured yet). About 200 lines that list modules and routes are shortened in the middle.

> harmonia-api@1.0.0 start:dev > nest start --watch [9:39:48 AM] Starting compilation in watch mode... 1 [9:39:54 AM] Found 0 errors. Watching for file changes. 8 (node:5374) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated. (Use `node --trace-deprecation ...` to show where the warning was created) [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [NestFactory] Starting Nest application... 2 [Nest] 5428 - 10/22/2026, 9:39:56 AM WARN [FirebaseAdmin] FIREBASE_SERVICE_ACCOUNT_JSON not set — bearer tokens cannot be verified. Set ALLOW_DEV_USER_HEADER=true to use x-user-id in development. 3 [Nest] 5428 - 10/22/2026, 9:39:56 AM ERROR [UploadsService] R2 not configured: set CLOUDFLARE_R2_ENDPOINT, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY, _BUCKET, _PUBLIC_URL. Uploads will fail. [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] TypeOrmModule dependencies initialized +2ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] ThrottlerModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] JwtModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] DiscoveryModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] ConfigModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] ScheduleModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] AppModule dependencies initialized +15ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +94ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms [... lines 25-240 omitted: 216 more "InstanceLoader ... dependencies initialized" / "RouterExplorer Mapped ..." lines ...] 4 [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [AdminAuthService] Seeded super admin admin@example.com [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [SubscriptionsService] Created default premium_page_settings 5 [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [SubscriptionsService] Created default ads_settings (AdMob test unit ids) [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [SubscriptionsService] Seeded 4 subscription plans [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [AppConfigService] Created default app_config [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [SearchService] Search: pg_trgm enabled (ranked, typo-tolerant) 6 [Nest] 5428 - 10/22/2026, 9:39:56 AM WARN [FFmpeg] FFmpeg not found. Install it (e.g. `sudo apt install ffmpeg`) or set FFMPEG_PATH. Stream quality versions and loudness analysis are disabled until then. [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [NestApplication] Nest application successfully started +5ms 7 [Nest] 5428 - 10/22/2026, 9:39:56 AM LOG [Bootstrap] 🚀 Server running on http://localhost:7001
  1. Found 0 errors: the TypeScript code compiled.
  2. WARN FirebaseAdmin: normal until you add FIREBASE_SERVICE_ACCOUNT_JSON (section 4, step 4). App sign-in is not verified until then.
  3. ERROR UploadsService "R2 not configured": shown in red but not fatal. The server keeps running; only file uploads fail until you complete step 6.
  4. Seeded super admin: your ADMIN_EMAIL account was created. Sign in to the admin panel with it.
  5. Created default …: default settings, Google's test ad IDs, sample Premium plans (development only) and app config were written to the database.
  6. WARN FFmpeg: appears only if FFmpeg is not installed (see step 1).
  7. Server running on http://localhost:7001: success. The API is ready.
  8. The grey DeprecationWarning comes from the Nest CLI's file watcher, not from Harmonia, and can be ignored.

If the output stops with a red error before line 7, compare it with the troubleshooting list below. The most common causes are a wrong database password or port, and a database that does not exist yet.

Check it in the browser or terminal

$ curl -s "http://localhost:7001/api/v1/app/config?platform=android"
{"data":{"min_version":"1.0.0","latest_version":"1.0.0","store_url":null,"force_update_message":"Please update to the latest version to keep listening.","maintenance_mode":false,"maintenance_message":"We are doing some maintenance. Back shortly.","privacy_url":null,"terms_url":null,"support_email":null},"isArray":false,"path":"/api/v1/app/config","duration":"5ms","method":"GET"}

A JSON reply like this (wrapped in data) means the API, the database and the default settings all work. In development, open http://localhost:7001/api in your browser to see the interactive Swagger API docs:

Swagger UI at /api showing Harmonia API 1.0
Swagger at http://localhost:7001/api after a successful first run (development only; it is switched off in production).

Step 5: Go live with PM2

npm ci
npm run build
npm install -g pm2
# edit cwd in prod.config.js, then:
pm2 start prod.config.js      # sets NODE_ENV=production
pm2 save && pm2 startup        # restart automatically after a reboot

Put nginx in front with HTTPS (for example with Let's Encrypt / certbot):

server {
    server_name api.yourdomain.com;
    client_max_body_size 250m;   # admin uploads: audio up to 60 MB, video up to 250 MB

    location / {
        proxy_pass http://127.0.0.1:7001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

To update later: upload the new files, then npm ci && npm run build && pm2 restart harmonia-api. New migrations run automatically on start.

Step 6: Media storage (Cloudflare R2)

Uploads from the admin panel are stored under <prefix>image/, <prefix>audio/ and <prefix>video/. Limits: images 10 MB, audio 60 MB, video 250 MB. Stream qualities created by ffmpeg go to <prefix>audio-variants/. Without R2 the upload endpoint answers 503 "Media storage is not configured"; you can still paste links to files hosted elsewhere (https only).

Step 7: Google Play (Premium)

A. Service account (purchase verification)

B. Real-time developer notifications (recommended) — tell the backend about renewals, cancellations and refunds.

Without notifications, subscription changes are picked up when the app re-verifies and by an hourly expiry job. Refunds and revocations are always re-checked with Google before Premium is removed.

Step 8: Expiring audio links (optional)

By default, audio URLs on your R2 domain are permanent. To make them expire, deploy the included worker docs/media-signing-worker.js:

Images and videos stay public; protected audio needs a valid signature, which the backend adds automatically.

Troubleshooting

ProblemFix
Server stops at start with a configuration errorRead the message; it names the variable to change (see the production safety check above).
ECONNREFUSED or "password authentication failed"Check DB_* values and that PostgreSQL is running and accepts connections.
"Server cannot verify tokens: FIREBASE_SERVICE_ACCOUNT_JSON is not configured"Set the Firebase service account (step 3). It must belong to the same Firebase project as the app.
"database … does not exist"Create it first (step 2), and check DB_NAME.
Configuration error about DB_PASS on a local PostgreSQL without a passwordDB_PASS may not be empty. Set any placeholder text; PostgreSQL ignores it when it trusts local connections.
"FFmpeg not found" warningInstall FFmpeg (step 1) or set FFMPEG_PATH, then restart.
Port 7001 already in useChange PORT in .env and in your nginx proxy_pass.
Uploads fail with 413Raise client_max_body_size in nginx.
Uploads fail with 503R2 variables are missing or wrong (step 6).
Purchase verification fails with 503Google Play service account not configured (step 7).

14. Admin Panel Setup (Next.js)

The admin panel lives in Admin-Panel/. It is a Next.js 15 web app that talks to the backend.

Step 1: Install and configure

cd Admin-Panel
npm ci
cp .env.example .env.local

Set NEXT_PUBLIC_API_URL to your backend including /api/v1:

NEXT_PUBLIC_API_URL=http://localhost:7001/api/v1          # local
NEXT_PUBLIC_API_URL=https://api.yourdomain.com/api/v1     # production
NEXT_PUBLIC_API_URL is built into the browser code. Never put a secret there, and run npm run build again after changing it.

Step 2: Run

npm run dev        # http://localhost:3000

Start the backend first. A successful start looks like this (real output):

> harmonia-admin@1.0.0 dev > next dev --turbopack ▲ Next.js 15.5.27 (Turbopack) - Local: http://localhost:3000 - Network: http://<your-computer-ip>:3000 3 - Environments: .env.local ✓ Starting... ✓ Compiled middleware in 65ms 1 ✓ Ready in 644ms ○ Compiling /login ... ✓ Compiled /login in 2.3s 2 GET /login 200 in 2517ms GET /login 200 in 44ms ✓ Compiled /favicon.ico in 126ms GET /favicon.ico?favicon.0b3bf435.ico 200 in 361ms
  1. Ready: the admin panel is running at http://localhost:3000.
  2. GET /login 200: your browser opened the sign-in page.
  3. Environments: .env.local: your settings file was found. If this line is missing, the panel calls the default http://localhost:7001/api/v1.

Open http://localhost:3000. You should see the sign-in page:

Harmonia admin sign-in page
The admin sign-in page. Sign in with the backend's ADMIN_EMAIL and ADMIN_PASSWORD.

After signing in you land on the Dashboard (see the admin panel tour below). On a new installation the numbers are 0 until you add songs. If sign-in fails with a network or CORS error, check that the backend is running and that CORS_ORIGINS in the backend's .env contains http://localhost:3000.

Step 3: Go live

npm ci
npm run build
# edit cwd in prod.config.js, then:
pm2 start prod.config.js     # runs "next start" on port 3000

Put nginx in front with HTTPS:

server {
    server_name admin.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Then add https://admin.yourdomain.com to the backend's CORS_ORIGINS and restart the backend. Vercel also works: import the project, set NEXT_PUBLIC_API_URL in the project settings and deploy.

Roles

RoleCan do
viewerRead the dashboard and content
editorEverything a viewer can, plus create, edit and delete content, banners, notifications, plans, ads and app config
super_adminEverything, plus staff accounts, the audit log, granting or revoking Premium and erasing app users

What you can manage

PageWhat it does
DashboardUsers, active listeners, Premium users, plays, likes, downloads, catalogue totals, plays per day, top songs
Search analyticsTop and zero-result searches, clicks; pin or hide trending searches
Home sectionsCreate, order and schedule Home sections: layout, content source (manual, new releases, trending, for you, recently played) and items
BannersHome banner carousel linking to an album, playlist, artist, Premium or a URL
SongsUpload audio and cover, lyrics, credits and licence, Premium flag, stream qualities (64/128/256 kbps), retry failed conversions, bulk upload from Excel (.xlsx) or CSV
Albums / Playlists / ArtistsCreate and edit with track pickers, Premium flags and bulk upload from Excel (.xlsx) or CSV
Moods & genresCategories shown on Explore and in onboarding
ShortsShort music clips
App usersSearch, view details, ban, grant or revoke Premium, erase a user
SubscriptionsSubscribers, estimated monthly revenue, plan mix, CSV export
Premium plansPlans with Google Play IDs, and the Premium screen texts
AdsAdMob unit IDs per platform and interstitial frequency
App configMinimum and latest app version (force update), maintenance mode, store links, privacy policy, terms, support e-mail
Push notificationsCompose, target (all, Premium, free or one user), schedule, test and send
Admin users / Audit logStaff accounts and a log of every change (super admin only)

Admin panel tour

What each main page looks like, so you know your way around before you add content. Screenshots are from the demo with sample music.

Dashboard: listeners, plays, catalogue totals, plays per day and top songs
Dashboard: listeners, plays, catalogue totals, plays per day and top songs
Home sections: the rows of the app's Home screen, top to bottom. Turn them on or off, reorder, schedule and edit contents
Home sections: the rows of the app's Home screen, top to bottom. Turn them on or off, reorder, schedule and edit contents
Banners: the carousel at the top of Home
Banners: the carousel at the top of Home
Songs: catalogue list with access, tags and stream quality status.
Songs: catalogue list with access, tags and stream quality status. "+ New song" opens the upload form; "Bulk upload" imports a spreadsheet
New song form: cover and audio file, artist, tags, Premium flag, stream qualities, credit, licence and synced lyrics
New song form: cover and audio file, artist, tags, Premium flag, stream qualities, credit, licence and synced lyrics
Albums: track list, the Home sections each album appears in, and Free or Premium access
Albums: track list, the Home sections each album appears in, and Free or Premium access
Artists: name, bio, photo, songs and albums
Artists: name, bio, photo, songs and albums
Playlists: catalogue playlists shown in the app
Playlists: catalogue playlists shown in the app
Moods and genres: shown on Explore and in the music-taste picker
Moods and genres: shown on Explore and in the music-taste picker
Shorts: short clips linked to a song, album or playlist
Shorts: short clips linked to a song, album or playlist
Premium plans: each plan's Google Play product, base plan and offer IDs, features and badge
Premium plans: each plan's Google Play product, base plan and offer IDs, features and badge
Subscriptions: subscribers, estimated monthly revenue and every purchase record (user details blurred)
Subscriptions: subscribers, estimated monthly revenue and every purchase record (user details blurred)
Ads: AdMob unit IDs per platform and interstitial frequency. The yellow notice warns while Google test IDs are still in use
Ads: AdMob unit IDs per platform and interstitial frequency. The yellow notice warns while Google test IDs are still in use
Push notifications: device counts, history and status of every campaign
Push notifications: device counts, history and status of every campaign
Notification composer: title, message, picture, audience, tap action, schedule and live preview
Notification composer: title, message, picture, audience, tap action, schedule and live preview

Troubleshooting

ProblemFix
Every request returns 404NEXT_PUBLIC_API_URL is missing /api/v1
Login fails with a network or CORS errorAdd the panel's address to the backend's CORS_ORIGINS and restart the backend
A changed .env.local value has no effectRun npm run build again
Port 3000 is in useChange -p 3000 in the start script in package.json
Forgot the admin passwordAnother super admin can set a new password through the API (PATCH /api/v1/admin/users/:id with password). If you are the only admin, delete your row from admin_users and restart the backend with new ADMIN_EMAIL/ADMIN_PASSWORD

15. API Reference

All endpoints live under https://api.yourdomain.com/api/v1. The full, always-current reference is the Swagger UI at /api on a development server. This section covers the conventions and the most important calls.

Conventions

The response envelope

Successful responses are wrapped like this:

{
  "data": { ... },          // the actual result
  "isArray": false,
  "path": "/api/v1/songs/3f2c...",
  "duration": "12ms",
  "method": "GET"
}

Some older endpoints put their own { "status", "message", "data" } object inside data; the app and admin panel handle both forms.

Errors

// 400 validation error
{ "statusCode": 400, "timestamp": "2026-01-15T10:00:00.000Z", "path": "/api/v1/songs/create",
  "message": ["thumbnail must be an https URL", "title should not be empty"], "error": "Bad Request" }

// 404
{ "statusCode": 404, "timestamp": "...", "path": "/api/v1/songs/...", "message": "Song not found", "error": "Not Found" }

// 401 when a device was signed out remotely
{ "statusCode": 401, "timestamp": "...", "path": "...", "code": "DEVICE_REVOKED",
  "message": "This device was signed out from another device" }
CodeMeaning
200 / 201 / 202 / 204Success (202 for play and search events, 204 for deletes without a body)
400Invalid input
401Missing, invalid or expired token; device signed out
403Admin role not allowed, or the app user is banned
404Not found
409Conflict, e.g. an admin with this e-mail exists, or a purchase belongs to another account
429Too many requests
500Unexpected server error ("Internal server error"; details are only in the server log)
503A required service is not configured (R2, Google Play, RTDN token)

Authentication

App users sign in with Firebase in the app. Every request sends the Firebase ID token and the install's device ID:

Authorization: Bearer <Firebase ID token>
x-device-id: <stable id per app install>

There is no separate register endpoint: the user record is created on the first authenticated request. Many read endpoints also work without a token (guest mode).

Admins log in with e-mail and password:

POST /api/v1/admin/auth/login
{ "email": "admin@yourdomain.com", "password": "your-password" }

// response (inside "data")
{ "access_token": "<JWT>", "expires_in": "24h",
  "admin": { "id": "uuid", "email": "admin@yourdomain.com", "name": "Super Admin",
             "role": "super_admin", "is_active": true, "last_login_at": "..." } }

Send the token as Authorization: Bearer <JWT> on admin requests. Other admin auth calls: GET /admin/auth/me and POST /admin/auth/change-password with { "current_password", "new_password" }.

Example: the Home feed

GET /api/v1/home?page=1&limit=10        (token optional)

{ "status": true, "message": "Home data fetched successfully",
  "pagination": { "page": 1, "limit": 10, "total": 7, "totalPages": 1, "isLastPage": true },
  "data": [
    { "title": "Banners", "type": "banner_carousel", "id": "banners",
      "items": [ { "id": "uuid", "title": "...", "image": "https://...", "redirectType": "playlist", "target_id": "uuid" } ] },
    { "title": "Top hits", "type": "song_horizontal_cards", "id": "uuid",
      "items": [ { "id": "uuid", "title": "...", "image": "...", "redirectType": "song", "is_premium": true,
                   "songs": [ { "id": "uuid", "title": "...", "artist": "...", "duration": 180,
                                "file_path": null, "audio_variants": null, "is_premium": true, "is_locked": true } ] } ] }
  ] }

Section types: banner_carousel, play_list_horizontal_cards, full_playlist_songs_horizontal_cards, song_horizontal_cards, artist_songs_horizontal_cards, horizontal_artist_list, shorts_horizontal_cards, recent_played_horizontal_cards, mood_chips_horizontal, genre_horizontal_cards.

Example: a song

GET /api/v1/songs/{id}        (token optional)

{ "id": "uuid", "title": "...", "artist": "Artist name", "featured": "Feat A",
  "thumbnail": "https://cdn.../cover.jpg", "duration": 215,
  "file_path": "https://cdn.../harmonia/audio/2026-10/x.mp3?exp=...&sig=...",
  "audio_qualities": ["low", "normal", "high"],
  "audio_variants": { "low": "https://...m4a", "normal": "...", "high": "..." },
  "variants_status": "ready",
  "lyrics": [ { "time": 0, "text": "First line" } ],
  "is_premium": false, "is_locked": false }

When a song is Premium and the caller is not, the response has "is_locked": true, "file_path": null and "audio_variants": null.

Example: search

GET /api/v1/search?q=love&limit=20&type=song

[ { "id": "uuid", "name": "Love Song", "type": "SONG", "image": "...", "artist": "...",
    "artist_id": "uuid", "duration": 200, "is_premium": false } ]

Also: GET /search/page?q=&offset=&limit= returns { "items": [...], "next_offset": 20 }, GET /search/suggest?q= and GET /search/trending.

Example: report playback

POST /api/v1/plays        (token optional, x-device-id recommended) → 202

{ "song_id": "uuid", "source_type": "playlist", "source_id": "uuid",
  "position_ms": 45000, "duration_ms": 215000, "completed": false, "platform": "android" }

A play counts after 30 seconds or when the song finishes. POST /plays/batch accepts { "events": [...] } with up to 200 events.

Example: verify a Google Play purchase

POST /api/v1/purchases/google/verify        (token required)

{ "purchase_token": "<from Google Play>", "product_id": "premium_single", "backend_plan_id": 1 }

// response
{ "is_premium": true, "status": "active", "platform": "android", "plan_id": 1,
  "product_id": "premium_single", "base_plan_id": "monthly",
  "expires_at": "2026-11-16T10:00:00.000Z", "auto_renew": true }

GET /me/entitlement returns the same object for the signed-in user.

Example: create a song (admin)

POST /api/v1/songs/create        (admin token, editor or super_admin)

{ "title": "My Song", "thumbnail": "https://cdn.yourdomain.com/.../cover.jpg",
  "file_path": "https://cdn.yourdomain.com/.../audio.mp3", "duration": 215,
  "artist_id": "uuid", "is_premium": false, "tags": "pop,summer",
  "lyric": [ { "time": 0, "text": "First line" } ],
  "audio_qualities": ["low", "normal", "high"],
  "credit": "Artist - Title", "license": "CC BY 4.0", "license_url": "https://...", "source_url": "https://..." }

title, thumbnail and file_path are required; thumbnail and file_path must be https URLs. Upload files first with POST /admin/uploads?kind=image|audio|video (returns { url, key, kind, mime, size }).

Endpoint groups

Every path below is relative to https://your-api/api/v1. The full, always-current list with request bodies is in Swagger at /api when NODE_ENV is not production.

Access: public (no token), optional (works for guests, richer when signed in), user (Firebase token), admin (any admin role), editor (editor or super admin), super (super admin).

GroupMethod and path (prefix /api/v1)Access
HealthGET /public
HomeGET /homeoptional
SongsGET /songs/:id
GET /songs/:id/radio
GET /credits
optional / public
Songs (admin)POST /songs/create
GET /songs/list
GET /songs/options
PATCH /songs/update/:id
DELETE /songs/:id
POST /songs/qualities
POST /songs/qualities/retry-failed
POST /songs/bulk-upload
admin read, editor write
ArtistsGET /artists
GET /artists/:id
POST /artists
PATCH /artists/:id
DELETE /artists/:id
POST /artists/bulk-upload
public / optional; editor write
AlbumsGET /album
GET /album/:id
GET /album/by-category/:categoryId
POST /album
PUT /album/:id
DELETE /album/:id
POST /album/bulk-upload
admin read, editor write
Catalogue playlistsGET /playlist
GET /playlist/:id
GET /playlist/by-category/:categoryId
POST /playlist
PUT /playlist/:id
DELETE /playlist/:id
admin read, editor write
CollectionsGET /category/collections/:categoryId
GET /category/collections/album-playlist-by-id/:id
GET /collections/:id/related
optional
Categories (home sections, moods, genres)GET /category
GET /category/:id
POST /category
PUT /category/:id
DELETE /category/:id
PUT /category/reorder
GET /category/admin/sections
GET /category/admin/:id/contents
PUT /category/admin/:id/contents
public read; admin / editor
ShortsGET /shorts/get-all-with-includes
GET /shorts/get-all
GET /shorts/:id
POST /shorts/:id/like
DELETE /shorts/:id/like
POST /shorts/create
PUT /shorts/:id
DELETE /shorts/:id
optional / user; editor write
User playlistsGET /playlists/me
GET /playlists/containing
POST /playlists
GET /playlists/:id
PATCH /playlists/:id
DELETE /playlists/:id
POST /playlists/:id/songs
DELETE /playlists/:id/songs/:songId
PUT /playlists/:id/songs/order
user
LibraryGET /library/items
GET /library/liked-songs
GET /library/liked-songs/stats
POST /library/liked-songs/:songId
DELETE /library/liked-songs/:songId
GET /library/saved/ids
POST /library/saved
DELETE /library/saved/:kind/:entityId
user
Plays and historyPOST /plays
POST /plays/batch
GET /me/continue-listening
DELETE /me/continue-listening/:songId
GET /me/history
DELETE /me/history
optional / user
SearchGET /search
GET /search/page
GET /search/suggest
GET /search/trending
POST /search/events
public / optional
ProfileGET /users/me
PATCH /users/me
POST /users/me/photo
DELETE /users/me
user
Music tasteGET /onboarding/options
GET /me/preferences
PUT /me/preferences
public / user
DevicesPOST /me/devices
GET /me/devices
DELETE /me/devices/session/:id
DELETE /me/devices/:token
optional / user
Premium and adsGET /premium/config?platform=android|ios
GET /ads/config
GET /me/entitlement
POST /purchases/google/verify
POST /purchases/google/rtdn
public / user / RTDN secret token
HealthGET /healthpublic; returns status: ok when the API and database are up (503 otherwise). Use it for uptime monitors and load balancers
App configGET /app/config?platform=android|ios
GET /admin/app-config
PUT /admin/app-config
public; admin read, editor write
Admin: sign-inPOST /admin/auth/login
GET /admin/auth/me
POST /admin/auth/change-password
public / admin
Admin: statsGET /admin/stats/overview
GET /admin/stats/top-songs
GET /admin/stats/daily-plays
GET /admin/stats/search
admin
Admin: bannersGET /admin/banners
POST /admin/banners
PATCH /admin/banners/:id
PUT /admin/banners/reorder
DELETE /admin/banners/:id
admin read, editor write
Admin: uploadsPOST /admin/uploads?kind=image|audio|videoeditor
Admin: push notificationsGET /admin/notifications
GET /admin/notifications/audience
GET /admin/notifications/:id
POST /admin/notifications
PATCH /admin/notifications/:id
POST /admin/notifications/test
POST /admin/notifications/:id/send
POST /admin/notifications/:id/cancel
POST /admin/notifications/:id/duplicate
DELETE /admin/notifications/:id
admin read, editor write
Admin: trending searchesGET /admin/search/trending
POST /admin/search/trending/overrides
DELETE /admin/search/trending/overrides/:id
admin read, editor write
Admin: Premium and adsGET /admin/premium/plans
POST /admin/premium/plans
PATCH /admin/premium/plans/:id
DELETE /admin/premium/plans/:id
GET /admin/premium/page-settings
PUT /admin/premium/page-settings
GET /admin/ads/settings
PUT /admin/ads/settings
admin read, editor write
Admin: subscriptionsGET /admin/subscriptions
GET /admin/subscriptions/stats
GET /admin/subscriptions/export.csv
admin
Admin: app usersGET /admin/app-users
GET /admin/app-users/:id
PATCH /admin/app-users/:id/ban
POST /admin/app-users/:id/grant-premium
POST /admin/app-users/:id/revoke-premium
DELETE /admin/app-users/:id
admin read; super admin for Premium, ban and erase
Admin: staff and auditGET /admin/users
POST /admin/users
PATCH /admin/users/:id
DELETE /admin/users/:id
GET /admin/audit-logs
GET /admin/audit-logs/actions
super admin

16. Database Schema

PostgreSQL, managed with TypeORM. Tables are defined in src/modules/*/entities/*.entity.ts; production changes ship as migrations in db/migrations/ and run automatically when the server starts.

Conventions

Tables by area

AreaTables
Cataloguesongs, song_artists, artists, album, album_songs, album_artists, album_categories, playlist, playlist_songs, playlist_categories, shorts, shorts_categories, shorts_likes, categories, category_artists, category_songs, banners
Users and libraryusers, user_liked_songs, user_saved_library_items, user_preferences, user_devices
Listeningplay_events, song_stats, user_listening, search_events, search_trending_overrides
Billing and adssubscription_plans, user_subscriptions, premium_page_settings, ads_settings
Operationsapp_config, push_notifications, admin_users, admin_audit_logs, migrations

Key tables

TableImportant columns
songstitle, thumbnail, file_path, duration (seconds), lyric (jsonb [{time,text}]), artist_id, is_premium, audio_qualities, audio_variants (jsonb), variants_status, loudness_lufs, credit, license, license_url, source_url
artistsname, bio, image_url, dominant_color
albumname, description, cover_url, is_premium, duration
playlistname, cover_url, owner_id (Firebase UID; empty for catalogue playlists), creator_type, is_public, is_premium
categoriesHome sections, moods and genres: name, kind, type (layout), source, item_order, position, is_active, starts_at, ends_at, show_on_explore
usersid (Firebase UID), email, name, photo_url, provider, is_banned, last_seen_at
user_subscriptionsuser_id, platform, purchase_token (unique), plan_id, product_id, status, expires_at, auto_renew
subscription_plansname, platforms, payment_providers, google_product_id, google_base_plan_id, google_offer_id, has_trial, trial_days, features, is_active, sort_order
play_eventsOne row per listening session: user_id (empty for guests), song_id, source_type, position_ms, duration_ms, completed, counted
user_devicestoken (FCM), user_id, platform, device_id, device_name, push_enabled, revoked_at
admin_usersemail (unique), password_hash (bcrypt, never returned), name, role, is_active
app_configSingle row: min/latest versions per platform, force-update and maintenance messages, store URLs, privacy and terms URLs, support e-mail

Enum values

EnumValues
Admin rolesuper_admin, editor, viewer
Category kindsection, mood, genre
Category sourcemanual, new_releases, trending, for_you, recently_played
Banner / notification targetnone (notifications only), album, playlist, artist, premium, url
Notification audienceall, premium, free, user
Notification statusdraft, scheduled, sending, sent, failed
Library saved kindplaylist, album, artist, download
Play sourcealbum, playlist, artist, shorts, search, library, autoplay, other
Subscription statuspending, active, canceled, in_grace_period, on_hold, paused, expired, revoked, unknown (Premium = active, canceled or in_grace_period with a future expiry)
Subscription platformandroid, ios, manual
Payment providergoogle_play, app_store (app_store is rejected while App Store purchases are switched off, see section 11)
Audio variant statusnone, pending, processing, ready, failed

Migrations

npm run migration:show       # list applied and pending migrations
npm run migration:run        # apply pending migrations manually
npm run migration:revert     # undo the last migration

A fresh production database is created entirely by the migrations on first start: 1793149601049-InitialSchema creates every table, index and foreign key of version 1.0.0. This has been tested on an empty PostgreSQL database with NODE_ENV=production.

Use an empty database for production. Development mode (NODE_ENV=development) creates tables directly from the code. If you point a production server at a database that development already filled, the first migration stops with relation "users" already exists. Either create a new empty database for production (recommended), or, if the development schema is already up to date and you want to keep its data, mark the first migration as applied once:
CREATE TABLE IF NOT EXISTS migrations (id serial PRIMARY KEY, "timestamp" bigint NOT NULL, name varchar NOT NULL);
INSERT INTO migrations ("timestamp", name) VALUES (1793149601049, 'InitialSchema1793149601049');

Backups

pg_dump -Fc -d harmonia -f harmonia-$(date +%F).dump      # back up
pg_restore -d harmonia --clean harmonia-2026-01-15.dump      # restore

Schedule a daily backup (for example with cron) and keep copies off the server. Media files are in R2, so back up the bucket separately if needed.

17. FAQ and Common Build Errors

Before anything else

Flutter app

Error or problemFix
"Unsupported class file major version" / Gradle fails to startYour JDK is too new. Use JDK 17 or 21: flutter config --jdk-dir "<Android Studio>/jbr"
"Your project's Gradle / Android Gradle Plugin / Kotlin version is lower than Flutter's minimum"Use Flutter 3.47.x as tested. On a newer Flutter, raise the versions in android/settings.gradle.kts and android/gradle/wrapper/gradle-wrapper.properties to the values in the message
"No matching client found for package name"google-services.json does not match applicationId. Download it again for your package name
Google sign-in fails on Android (error 10 / DEVELOPER_ERROR)Add the SHA-1 and SHA-256 of the key you are running with (debug, upload or Play app signing) in Firebase, then download google-services.json again
Google sign-in crashes on iOSAdd the REVERSED_CLIENT_ID URL scheme (section 5)
Nothing loads; every screen is emptyCheck apiHost (section 8). On an Android emulator use http://10.0.2.2:7001, not localhost
Sign-in works but API calls return 401The backend's Firebase service account belongs to a different Firebase project than the app
Songs show a lockThey are Premium (or inside a Premium album). Sign in with a Premium account, or grant Premium from the admin panel
CocoaPods errors (pod install fails)cd ios && pod repo update && pod install. If it still fails, delete ios/Pods and ios/Podfile.lock and run pod install again
iOS "No signing certificate" / "requires a development team"Select your Team in Xcode (Runner > Signing & Capabilities)
iOS minimum version errors from podsSet the Runner target's Minimum Deployments to 15.0 to match the Podfile
Push notifications do not arrive on iOSAdd the Push Notifications capability and upload an APNs key in Firebase (section 5). Test on a real device
Premium plans do not appearThe app is not installed from Google Play (use a testing track), the plan IDs do not match Play Console, or the plan is inactive in the admin panel
Ads show "Test Ad"Expected with the default test IDs. Set your own App ID (section 10) and unit IDs (admin panel > Ads)

Backend and admin panel

See the troubleshooting tables in section 13 and section 14.

General questions

QuestionAnswer
Is music included?No. The demo uses Creative Commons tracks for demonstration only. You must own or license every track you publish.
Can I sell Premium on iOS?Not in this version: Apple In-App Purchase verification is not included. iOS listeners use the free tier, and you can grant Premium manually from the admin panel.
Can I add another payment gateway?Yes, as custom development. Premium is stored in user_subscriptions, so a new provider only needs to create rows there after a verified payment.
Do I need Cloudflare R2?Yes for uploads from the admin panel. You can also paste https links to files hosted elsewhere.
Can I host the backend and admin panel on one server?Yes. Use two nginx server blocks (for example api. and admin. subdomains).
Are any files minified or obfuscated?No. All three projects ship as readable source code. Third-party libraries are installed from pubspec.yaml and package.json.

18. Support

Questions before or after purchase? Please use the Support tab on the CodeCanyon item page. All support is handled through the official Envato item support system.

We answer through the item's Support tab. Support covers bugs and questions about the included features; customisation and installation on your server are not part of item support.

Before you open a ticket

What to include

A complete first message lets us answer in one reply instead of several:

InformationHow to get it
Your purchase codeCodeCanyon > Downloads > Harmonia > License certificate and purchase code
Which partFlutter app, backend or admin panel, and the documentation section you were following
Platform and versionsAndroid or iOS and the device or emulator; your computer's OS; for the app the output of flutter doctor -v; for the server the output of node -v and psql --version and the server's OS
The exact error messageCopy and paste the text, not a summary. A screenshot of the full screen or terminal helps too
Log outputApp: the red error lines from flutter run. Backend: pm2 logs harmonia-api --lines 100. Admin panel: pm2 logs harmonia-admin --lines 100 and the browser console (F12 > Console)
Steps to reproduceWhat you did, what you expected, and what happened instead
Never send secrets. Remove passwords, JWT_SECRET, service-account JSON, API keys and keystore passwords from anything you paste. We will never ask for them.

More resources

19. Changelog

Version 1.0.0 (Initial Release)

AI disclosure: AI tools were used to assist with parts of this item (source code, documentation and preview graphics). All AI-assisted work was reviewed, edited and tested by the author.

20. Upgrading to a New Version

When a new version of Harmonia is released, download it from your CodeCanyon Downloads page and read its changelog first. Your songs, users, playlists, subscriptions and settings live in your PostgreSQL database and your R2 bucket, not in the code, so updating the code does not delete them.

Before you start

Carrying your own code changes forward

If you edited source files in the backend or admin panel (not only settings), do not copy the new version straight over them. The safest way is Git:

# one time, before your first customisation
cd Backend            # or Admin-Panel
git init && git add -A && git commit -m "Harmonia 1.0.0 original"
# ...make your changes and commit them as usual...

# when a new version arrives
git checkout -b upgrade
# copy the new package files over the folder (keep the files listed above)
git add -A && git commit -m "Harmonia 1.x.x original"
git checkout main && git merge upgrade      # Git combines both; fix any conflicts it lists

Files buyers commonly customise, and what to check after merging:

ProjectCommonly customisedAfter upgrading
Backendsrc/modules/subscriptions/premium.defaults.ts (default plans, ad settings), src/modules/*/ services you extended, docs/media-signing-worker.jsRun npm run build and npm test; new migrations run on start. Never edit an existing file in db/migrations/; add a new migration instead (npm run migration:generate)
Admin paneltailwind.config.ts (brand colours), app/layout.tsx (page title), components/sidebar.tsx (logo and name), pages in app/(admin)/Run npm run lint and npm run build

Without Git, compare your old folder and the new one with a diff tool (VS Code: right-click two files > Compare Selected) and re-apply your edits to the new files.

1. Backend

cd Backend
# copy the new files over your old folder, keeping your .env
npm ci
npm run build
pm2 restart harmonia-api

2. Admin panel

cd Admin-Panel
# copy the new files over your old folder, keeping .env.local
npm ci
npm run build
pm2 restart harmonia-admin

The admin panel stores nothing itself; all data comes from the backend. Upgrade the backend first.

3. Flutter app