Bookly App Documentation

Welcome to the official customization documentation for Bookly. Please follow the setup guide below before modifying the source code.

What is in this package: the Flutter mobile app, the NestJS backend API, and the Next.js admin panel. 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 admin panel both need a running backend.

1. Prerequisites

For optimal compatibility and performance, we highly recommend using the exact versions listed below.

Flutter SDK Min: 3.44.x
Java Version Java 21.0.9
CocoaPods Min: 1.16.2
Xcode Min: 16.x
Supported IDEs Android Studio / VS Code
Important: It is highly recommended to use Java 21.0.9 for your Android projects to avoid build errors and ensure optimal compatibility with the app's dependencies.
Need Help? If you are facing any version-related issues, please reach out to us directly! We are happy to help you. Go to the Support Section →

2. Environment Setup

Select your operating system below to view the setup instructions.

Android Studio Setup

  • Download Android Studio from the official page. We recommend setting your system Java environment to Java 21.0.9.
  • Run the downloaded executable (e.g., android-studio-ide-<version>-windows.exe) and follow the installation wizard.
  • Launch Android Studio and allow it to download additional SDK components.
  • Optional: Set up an emulator through the AVD (Android Virtual Device) Manager.

Flutter SDK Setup

  • Download the latest stable Windows release from the Flutter download page.
  • Extract the zip file to a location on your machine (e.g., C:\flutter).
  • Add to System Path: Right-click "This PC" > Properties > Advanced system settings > Environment Variables. Find the "Path" variable, click Edit, and add your flutter\bin directory path.
  • Open Command Prompt and run flutter doctor to verify installation.

IDE Configuration

  • Open Android Studio.
  • Go to File > Settings > Plugins > Marketplace.
  • Search for and install both the Flutter and Dart plugins.
  • Restart Android Studio.
▶ Watch Windows Setup Video

Android Studio Setup

  • Download Android Studio from the official page. We recommend setting your system Java environment to Java 21.0.9.
  • Open the downloaded DMG file and drag Android Studio into the Applications folder.
  • Launch Android Studio and complete the Setup Wizard, allowing it to install additional SDKs.

Flutter SDK Setup

  • Download the latest stable macOS release from the Flutter download page.
  • Open a terminal and extract the archive: tar xf flutter_macos_<version>.tar.xz
  • Move the extracted folder to your preferred location: sudo mv flutter /YOUR_DIRECTORY
  • Add the flutter/bin directory to your system's PATH variable in your shell profile (e.g., `.zshrc` or `.bash_profile`).
  • Open a new terminal and run flutter doctor to verify installation.

IDE Configuration

  • Open Android Studio.
  • Go to Android Studio > Preferences > Plugins > Marketplace.
  • Search for and install both the Flutter and Dart plugins.
  • Restart Android Studio.
▶ Watch macOS Setup Video

Android Studio Setup

  • Download Android Studio from the official page. We recommend setting your system Java environment to Java 21.0.9.
  • Open a terminal, navigate to your download directory, and extract the archive: tar -xvzf android-studio-ide-<version>-linux.tar.gz
  • Move the folder to your preferred location: sudo mv android-studio /YOUR_DIRECTORY
  • Navigate to the bin directory (cd /YOUR_DIRECTORY/android-studio/bin) and run ./studio.sh to start the setup wizard.

Flutter SDK Setup

  • Download the latest stable Linux release from the Flutter download page.
  • Extract the archive: tar xf flutter_linux_<version>.tar.xz
  • Move the extracted folder: sudo mv flutter /YOUR_DIRECTORY
  • Add the flutter/bin directory to your system's PATH variable in your shell profile.
  • Open a new terminal and run flutter doctor to verify installation.

IDE Configuration

  • Open Android Studio.
  • Go to File > Settings > Plugins > Marketplace.
  • Search for and install both the Flutter and Dart plugins.
  • Restart Android Studio.
▶ Watch Linux Setup Video
Need Help? If you are facing any environment setup related issues, directly reach out to us! We are happy to help you. Go to the Support Section →

3. Change Android Package Name

First, you have to find out the existing applicationId. You can find it out from the top of the /android/app/build.gradle.kts file.

Now select the application ID and press command + shift + r or for Windows ctrl + shift + r, place your preferred package name in the replace input box, and then click on the Replace All button.

4. Firebase Setup

Important: You should only add the downloaded Firebase JSON/Plist file. There is no change needed in any other files in the project like build.gradle or others.

Step 1: Create a Firebase Project

Step 2: Add Android App

Step 3: Add iOS App


Video Tutorials

If you prefer a visual guide, you can also watch the videos below for a complete setup walkthrough:

5. Social Sign In Config

To enable social authentication methods for your app, you need to configure the providers in your Firebase console.

Apple Sign-In for Android: An additional configuration is required to successfully implement Apple Sign-In for Android devices.

You need to place your Apple Service ID and Redirect URL in the project configuration file located at lib/core/config/app_config.dart.

For detailed instructions on how to set up your Service ID and Redirect URL, please follow this guide:

6. Change App Name

You need to set your app name in three different places: within your application's localization files, Android manifest, and iOS configuration file.

Localization Files

Android

android:label="YOUR_APP_NAME"

iOS

<key>CFBundleName</key>
<string>YOUR_APP_NAME</string>

7. Change App Logo / Placeholder Image and Icon

To change the App logo and App Icon, you need to follow these steps:

App Logo & Placeholder Image

Note: Please use the exact file names as described; otherwise, it will not work.

App Icon

You can generate your app icon using the tools shown in this tutorial:

8. API Customization

To connect the Bookly app to your own backend server, you must update the API endpoint configurations.

Important: Ensure that your custom backend API returns responses in the exact same JSON format and data structure as the default Bookly API, otherwise the app will not parse the data correctly.

9. Localization (Language Customization)

Bookly supports multi-language capabilities. You can easily add new languages, remove existing ones, or set a default app language by following these steps.

Add a New Language

Set the Default Language

Remove a Language

10. AdMob Customization

To start earning revenue through ads, you need to integrate your own AdMob Unit IDs for both Android and iOS.

CRITICAL WARNING: For development, you do not need to set up your real AdMob account. You must only configure AdMob with your real production IDs right before publishing your app to the App Store or Play Store. If you run the app during development and testing using your live production AdMob configuration, it will generate invalid traffic and your AdMob account will likely be permanently suspended.

The app is pre-configured to automatically use Google's provided test IDs during debug mode, ensuring your account remains safe while developing.

Update AdMob Unit IDs

The app configuration uses a conditional check (kDebugMode) to automatically switch between Test IDs (during development) and Production IDs (when live).

Need help creating Ad Unit IDs?

If you do not have an AdMob account or need help generating Ad Unit IDs, you can watch the video below for a clear understanding of the account creation process.

Note: The video below shows complete ad integration coding for Flutter. You only need to watch it to understand how to create your AdMob account, add your apps, and generate your Banner and Rewarded Ad Unit IDs. Do not follow the coding steps in the video, as the ad implementation is already fully integrated into the app.

Update AdMob Application IDs

In addition to the Unit IDs, you must configure your AdMob Application IDs in the native Android and iOS configuration files.

<key>GADApplicationIdentifier</key>
<string>YOUR_IOS_ADMOB_APP_ID</string>

11. In-App Purchase Configuration (RevenueCat)

Bookly uses RevenueCat to handle In-App Purchases (subscriptions and one-time purchases) seamlessly across Android and iOS platforms. Follow these steps to configure your products.

Update RevenueCat API Keys

Create Products in Google Play Console (and App Store)

Connect Stores to RevenueCat

Import Products & Configure Offerings in RevenueCat

12. Build and Release

Build for Android (APK)

Run the following command to build the APK:

flutter build apk

This command will compile your Flutter code and generate the APK file. You can find the APK in the build/app/outputs/flutter-apk directory inside your project folder. The APK file will be named something like app-release.apk or app-debug.apk.

Prepare for Google Play Store (App Bundle)

Before deploying to the Play Store, additional configuration is needed. You must create an upload keystore.

1. Create an upload keystore

Run the following command at the command line. On macOS or Linux, use the following command:

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

On Windows, use the following command in PowerShell:

keytool -genkey -v -keystore $env:USERPROFILE\upload-keystore.jks `
        -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 `
        -alias upload
Important: After running this keystore generation command, you need to enter some information. Make sure the keystore password is set carefully.

2. Replace the Keystore File

After generating upload-keystore.jks, replace the existing project bookly-keystore.jks with your new upload-keystore.jks in the <project>/android/app directory.

3. Update Key Properties

Update storePassword, keyPassword, and storeFile with your own credentials in <project>/android/key.properties.

4. Update App Version

Update your app version in <project>/android/app/build.gradle.kts.

5. Generate the App Bundle

Now you have completed all the release configurations. Generate the app bundle using the following command:

flutter build appbundle

After a successful build, you will find your app bundle in <project>/build/app/outputs/bundle/release/app-release.aab. Now you can upload it to your Google Play Console.

Build for iOS

There is no general way to generate apps for iOS locally without the Apple ecosystem. Apple doesn’t allow you to install apps via direct download. If you want to install it on your iOS device for testing, you can run it directly using Xcode. For deploying to production, please follow the official Flutter documentation here: https://docs.flutter.dev/deployment/ios

Run on a Physical iOS Device

Example: Running on an iPhone 13

Helpful Resource: For a more detailed guide on running Flutter on physical devices, check out this excellent article: Running Flutter on a Physical Device.

13. Backend Setup (NestJS API)

The backend powers both the mobile app and the admin panel. Set this up first — the app and admin panel cannot run without it.

Node.js 20 LTS or newer
PostgreSQL 14 or newer
Process Manager PM2 (production)
Stack NestJS 11 + TypeORM

Step 1: Install dependencies

Step 2: Create the database

Step 3: Configure your environment

VariableWhat to set
DB_HOST
DB_PORT
DB_USER
DB_PASSWORD
DB_NAME
Your PostgreSQL connection details.
BASE_URL The public address of this API, with no trailing slash.
Example: https://api.your-domain.com
JWT_SECRET
REFRESH_TOKEN_SECRET
Two different long random strings. Generate each with:
openssl rand -hex 48
BOOTSTRAP_ADMIN_EMAIL
BOOTSTRAP_ADMIN_PASSWORD
The first admin account. This is what you sign into the admin panel with.
DB_SYNC Set to true for the very first run so the database tables are created for you.
APP_LOCALES Active content languages, comma separated. Supported: en, bn, ar, hi
Never reuse the example secrets. JWT_SECRET and REFRESH_TOKEN_SECRET sign your users' login tokens. If you leave the placeholder values in place, anyone can forge a login for any account, including an administrator.

Step 4: First run

Backend is live. You can now set up the admin panel (section 14) and point the mobile app at this API (section 8).

Step 5: Lock it down after the first login

Once you have signed into the admin panel successfully, make these three changes in .env and restart the server:

Good to know: the bootstrap only runs while the database has no users at all. It will never overwrite an existing account, so leaving the variables in place is harmless — but removing them keeps the password out of your config file.

Step 6: File storage

Book covers, avatars, banners and the book files themselves (EPUB/PDF) need somewhere to live. The backend picks a storage driver automatically — the first fully configured one wins:

To use Cloudflare R2, fill in all of these in .env:

CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_R2_ACCESS_KEY_ID=
CLOUDFLARE_R2_SECRET_ACCESS_KEY=
CLOUDFLARE_R2_BUCKET=
CLOUDFLARE_R2_ENDPOINT=
CLOUDFLARE_R2_PUBLIC_URL=
Common mistake: if even one of those values is missing, R2 is skipped silently and the backend quietly falls back to local disk. There is no error message. If you expected files in your bucket but they keep appearing in the uploads/ folder, check that all six values are filled in — especially CLOUDFLARE_R2_PUBLIC_URL, which is easy to overlook.

Step 7: Optional integrations

All of these are switched off by default. The server starts and runs normally with every one of them unconfigured, so set up only what you need.

FeatureVariablesNotes
Push notifications
Firebase sign-in
FIREBASE_PROJECT_ID
FIREBASE_CLIENT_EMAIL
FIREBASE_PRIVATE_KEY
Taken from your Firebase service-account JSON (Project settings → Service accounts). Use the same Firebase project you set up for the app in section 4.
Email (OTP, password reset) MAIL_ENABLED
SMTP_HOST, SMTP_PORT
SMTP_USER, SMTP_PASS
SMTP_FROM
Set MAIL_ENABLED=true to turn it on. Use SMTP_SECURE=true for port 465, false for 587.
PipraPay payments PIPRAPAY_ENABLED
PIPRAPAY_API_KEY
PIPRAPAY_RETURN_URL
PIPRAPAY_WEBHOOK_URL
Set PIPRAPAY_ENABLED=true to turn it on.
In-app purchase receipts GOOGLE_PLAY_PACKAGE_NAME
APPLE_BUNDLE_ID, APPLE_ISSUER_ID
APPLE_KEY_ID, APPLE_PRIVATE_KEY
Needed only if you sell through the App Store / Play Store. See section 11.
Background jobs PG_BOSS_ENABLED
PG_BOSS_SCHEMA
Used for scheduled work such as rental-expiry reminders. Without it the app falls back to in-process scheduling, which is fine for a single server.
Firebase private key formatting: paste the key on a single line, wrapped in double quotes, keeping the \n escape sequences exactly as they appear in the JSON file:
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n"
If your hosting panel mangles that, use FIREBASE_PRIVATE_KEY_BASE64 instead and paste the same key base64-encoded.

Step 8: Going live with PM2

Where do the secrets go? prod.config.js contains no credentials by design. The app reads them from the .env file sitting in the folder named by cwd, so make sure your .env is uploaded there.

Step 9: nginx configuration

If you serve the API through nginx, use the settings below. Both matter:

client_max_body_size 50m;

location / {
    proxy_pass http://127.0.0.1:5008;
    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;
}

Step 10: Security settings

These are enabled out of the box. You may want to tune them for your deployment:

VariableDefaultWhat it does
CORS_ORIGINS empty Comma-separated list of web addresses allowed to call the API. Empty means any origin is allowed — fine locally, but in production set it to your admin panel's address. Mobile apps are unaffected either way.
RATE_LIMIT_MAX
RATE_LIMIT_TTL
120 per 60s General request limit per visitor.
AUTH_RATE_LIMIT_MAX
AUTH_RATE_LIMIT_TTL
20 per 60s Tighter limit on login, registration, OTP and password reset, to stop password-guessing attacks.
PROTECT_BOOK_FILES false Set to true to add an expiring signature to book file links, so a link that leaks cannot be reused. Your apps need no changes for this — the signature travels inside the link.
Paid books are already protected. Regardless of the setting above, the book file link is only ever given to a reader who has bought, rented or subscribed to that title. Free and ad-supported books remain open to everyone.

Troubleshooting

ProblemSolution
Warning at startup: Firebase is not configured Normal if you have not set up push notifications. The server runs fine. Fill in the FIREBASE_* values to remove it.
Cannot sign into the admin panel The admin account is only created when the database is completely empty. Check the startup logs for the Superadmin bootstrap complete line.
No tables were created DB_SYNC=true is required for the first run.
413 error when uploading a book Raise client_max_body_size in nginx (see step 9).
429 error when logging in Rate limit reached. Wait a minute, raise AUTH_RATE_LIMIT_MAX, or check that nginx forwards X-Forwarded-For.
CORS errors in the admin panel Add the admin panel's address to CORS_ORIGINS.
Files are not going to Cloudflare R2 One of the six R2 values is missing. See the warning in step 6.
Swagger shows the wrong address Set SWAGGER_SERVER_URL to your public API address.

14. Admin Panel Setup (Next.js)

The admin panel is where you manage books, authors, users, subscriptions and everything else in your store. Complete the backend setup (section 13) first — the panel is only a front-end for that API.

Node.js 20 LTS or newer
Stack Next.js 16 + React 19
Requires A running backend
Default Port 3000 dev / 4000 live

Step 1: Install dependencies

Step 2: Point it at your backend

VariableWhat to set
NEXT_PUBLIC_API_URL Your backend address including /api/v1.
Example: https://api.your-domain.com/api/v1
NEXT_PUBLIC_IMAGE_URL The address your uploaded files are served from, without any /uploads ending.
Local storage: https://api.your-domain.com
Cloudflare R2 / Bunny: your CDN address
NEXT_PUBLIC_APP_LOCALES Active languages, comma separated. Must match the backend's APP_LOCALES exactly.
Example: en,bn,ar,hi
These two are the most common setup mistakes — please read carefully:
A note on these settings: anything beginning with NEXT_PUBLIC_ is built into the browser files, so never put a password or secret key in this file. You must also run npm run build again after changing any of them — editing .env alone will not update an already-built panel.

Step 3: Run it

You are in. Start by adding a few categories and authors, then add your first book. Change your password from the profile menu.

Step 4: Going live

Do not forget the backend setting: once the panel is live, add its web address to CORS_ORIGINS in the backend's .env and restart the backend. Without this the browser blocks every request and the panel will not load any data.
CORS_ORIGINS=https://admin.your-domain.com

What you can manage

SectionWhat it covers
DashboardTotals for users, books, authors, publishers and categories.
BooksAdd and edit books, upload EPUB/PDF files and covers, set pricing, publish or unpublish.
Authors / Publishers / CatalogManage the people and categories behind your catalogue.
UsersView and manage accounts and their roles.
Subscription Plans & PricesSet up your plans, pricing and rental durations.
Book Sales / Subscription SalesReview orders and active subscriptions.
Home & BannersArrange the app home screen sections and promotional banners.
NotificationsSend push notifications to your users (requires Firebase).
ReviewsModerate reader reviews and comments.
Legal PagesEdit your Privacy Policy and Terms, which the app displays.
App VersionControl the in-app update prompt.

Multi-language content

Books, authors, publishers and categories can hold a separate title and description per language. The language tabs shown in the panel come from NEXT_PUBLIC_APP_LOCALES.

Keep both sides in sync. If the panel's NEXT_PUBLIC_APP_LOCALES and the backend's APP_LOCALES do not match, you may enter content in a language the backend then discards.

Troubleshooting

ProblemSolution
Every page is empty and all requests return 404 NEXT_PUBLIC_API_URL is missing the /api/v1 ending. Fix it, then run npm run build again.
Images and covers do not load Either NEXT_PUBLIC_IMAGE_URL wrongly ends in /uploads, or your image host is not allowed. For a separate CDN, add its hostname to NEXT_PUBLIC_EXTRA_IMAGE_HOSTS.
Browser console shows CORS errors Add the panel's address to CORS_ORIGINS in the backend .env and restart the backend.
Login says the details are wrong Use the BOOTSTRAP_ADMIN_* details from the backend .env. Confirm the backend created the account by checking its startup logs.
Too many login attempts / 429 The backend rate limiter has kicked in. Wait a minute and try again.
Changing .env has no effect Settings are baked in when the panel is built. Run npm run build and restart.
Signed out unexpectedly The login token has expired. Sign in again, or increase JWT_EXPIRES_IN in the backend .env.

15. API Reference

This section covers the request and response formats you will work with most often. The complete, always-current reference for all 166 endpoints is the Swagger UI served by your own backend:

http://localhost:5008/api/docs

Swagger is generated from the source code, so it can never drift out of date. Use Authorize there to paste a token and try any endpoint live.

Conventions

ItemValue
Base path/api
VersionIn the URL: /api/v1/...
Auth headerAuthorization: Bearer <access_token>
Content typeapplication/json (uploads use multipart/form-data)
IDsUUID v4
TimestampsISO 8601 UTC, e.g. 2026-09-19T10:30:00.000Z

The response envelope

Every response — success or failure — uses the same outer shape.

Single item

{
  "success": true,
  "status_code": 200,
  "data": { }
}

Lists

List endpoints add a pagination block. Some also add counts, which are totals across the whole resource and are not affected by your current search filter.

{
  "success": true,
  "status_code": 200,
  "data": [ ],
  "pagination": {
    "total": 120,
    "page": 1,
    "limit": 20,
    "totalPages": 6,
    "has_next": true,
    "has_prev": false,
    "counts": {
      "total_non_deleted": 120,
      "published_live": 98,
      "draft": 15,
      "pending": 7
    }
  }
}

Common list query parameters: page, limit, search, sort, status.

Errors

{
  "success": false,
  "status_code": 401,
  "error": "Unauthorized",
  "message": "Authorization token is missing. Send a Bearer token in the Authorization header.",
  "timestamp": "2026-09-19T10:30:00.000Z"
}

Validation failures return message as an array of strings, one per invalid field:

{
  "success": false,
  "status_code": 400,
  "error": "Validation Error",
  "message": [
    "fullName must be longer than or equal to 2 characters",
    "otp_token must be a string"
  ],
  "timestamp": "2026-09-19T10:30:00.000Z"
}
Handling errors in your client: always check success first, then read status_code. Remember that message is a string for normal errors but an array for validation errors — handle both.
CodeMeaning
200 / 201Success
400Validation failed, or a feature is not configured (e.g. SMTP)
401Missing, invalid or expired token
403Signed in, but not allowed — or an expired signed book link
404Not found
409Conflict, e.g. the email is already registered
413Upload too large — see the nginx note in section 13
429Rate limit reached

Multilingual fields

Titles, names and descriptions are objects keyed by language code, not plain strings. Which languages are accepted comes from APP_LOCALES.

{
  "title": {
    "en": "Moby Dick",
    "bn": "মোবি ডিক",
    "ar": "موبي ديك",
    "hi": "मोबी डिक"
  }
}

You do not have to send every language. Any you leave out come back as an empty string.

Authentication

Sign-up is a three-step flow

Registration is protected by an email one-time code, so it takes three calls.

SMTP is required for sign-up. Step 1 returns 400 "SMTP not configured" until you set MAIL_ENABLED=true and fill in the SMTP_* values (section 13, step 7). Existing accounts can still log in without SMTP — only new registrations are blocked.

Step 1 — request a code. POST /api/v1/auth/otp/send

{
  "email": "jane.doe@example.com",
  "purpose": "register"
}

purpose is one of register, forgot_password or email_verify.

Step 2 — verify the code. POST /api/v1/auth/otp/verify

{
  "email": "jane.doe@example.com",
  "purpose": "register",
  "otp": "482913"
}

The response contains an otp_token. It is short-lived — its lifetime is OTP_TOKEN_EXPIRES_IN, 10 minutes by default.

Step 3 — create the account. POST /api/v1/auth/register

{
  "fullName": "Jane Doe",
  "email": "jane.doe@example.com",
  "password": "StrongPa$$123",
  "otp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",

  "device_id": "9774d56d682e549c",
  "fcm_token": "fKx9...",
  "platform": "android",
  "device_name": "Google Pixel 8",
  "app_version": "1.0.0"
}
Note the spelling: this field is fullName in camelCase, while most other fields use snake_case. Sending full_name returns a validation error.

The device_* and fcm_token fields are optional. Send them to register the device for push notifications.

Login

POST /api/v1/auth/login

{
  "email": "jane.doe@example.com",
  "password": "StrongPa$$123"
}

Response:

{
  "success": true,
  "status_code": 201,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user": {
      "sub": "84138aab-093d-467d-834d-b902a2738c71",
      "email": "jane.doe@example.com",
      "roles": ["user"]
    },
    "subscription": {
      "is_active": false,
      "plan": "free",
      "subscription_id": null,
      "plan_id": null,
      "plan_slug": null,
      "plan_name": null,
      "status": null,
      "billing_cycle": null,
      "starts_at": null,
      "expires_at": null,
      "gateway": null,
      "can_read_subscription_books": false
    }
  }
}

user.sub is the user's UUID. Store both tokens; send the access token on every request.

Refreshing an expired token

Access tokens expire after JWT_EXPIRES_IN (4 hours by default). When a request returns 401, exchange your refresh token for a new pair rather than forcing the user to log in again.

POST /api/v1/auth/refresh

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Other auth endpoints: POST /auth/logout, POST /auth/forgot-password, POST /auth/reset-password, POST /auth/change-password, POST /auth/firebase (social sign-in).

Reading the catalogue

Book list

GET /api/v1/books?page=1&limit=20&search=moby — returns the paginated envelope shown above.

Book details

GET /api/v1/books/{id} — send the user's token so the response reflects what that reader is entitled to.

{
  "success": true,
  "status_code": 200,
  "data": {
    "id": "9e3fffc1-edb0-4390-b33e-52a3fd239ef5",
    "title": { "en": "Moby Dick", "bn": "মোবি ডিক" },
    "description": { "en": "..." },
    "author": {
      "id": "…", "name": { "en": "Herman Melville" },
      "image_url": "https://api.example.com/uploads/authors/x.jpg",
      "total_books": 12
    },
    "file_path": "https://api.example.com/uploads/books/x.epub",
    "preview_file_path": "",
    "cover_url": "https://api.example.com/uploads/covers/x.jpg",
    "total_chapters": 135,
    "is_favorite": false,
    "language": "English",
    "dominant_color": "#2B3A42",
    "categories": [ { "id": "…", "name": { "en": "Fiction" } } ],
    "access_control": {
      "can_access": true,
      "access_type": "purchase",
      "purchased_at": "2026-09-01T10:00:00.000Z",
      "expires_at": null,
      "is_free": false,
      "is_ads_supported": false,
      "is_subscribable": true,
      "is_rentable": true,
      "is_lifetime_purchasable": true
    },
    "pricing": {
      "currency": "USD",
      "rent_price": 2.99,
      "rent_discount_percent": 0,
      "lifetime_price": 9.99,
      "lifetime_discount_percent": 0,
      "rent_duration_in_days": 30
    },
    "review": { }
  }
}
Why file_path may be an empty string. The book file link is only included for a reader who is entitled to that title — one who bought it, rented it, or has an active subscription — plus anyone at all for free and ad-supported books. For everyone else it is "", which is the same value a book with no file uploaded yet returns.

Use access_control.can_access to decide whether to show the reader or the purchase screen. Do not treat an empty file_path as an error.

Creating content (admin)

Admin endpoints need a token belonging to a user with the admin role. Multilingual fields are sent as objects.

POST /api/v1/categories

{
  "name": { "en": "Fiction", "bn": "কল্পকাহিনী" },
  "description": { "en": "Novels and short stories" },
  "slug": "fiction"
}

Response:

{
  "success": true,
  "status_code": 201,
  "data": {
    "id": "e00f2495-11d0-4bc0-abc4-5cd3ceb0f440",
    "type": null,
    "name": { "en": "Fiction", "bn": "কল্পকাহিনী", "ar": "" },
    "thumbnail": null,
    "color": null,
    "description": { "en": "Novels and short stories", "bn": "", "ar": "" },
    "total_books": 0,
    "see_all": false,
    "books": []
  }
}

File uploads

Uploads use multipart/form-data, not JSON.

POST /api/v1/uploads/file
Authorization: Bearer <token>
Content-Type: multipart/form-data

file: <binary>

Limits: images up to 10 MB, EPUB/PDF up to 100 MB. Exceeding the limit returns 413 — if that happens on a server, raise client_max_body_size in nginx as well (section 13).

Endpoint groups

PrefixPurpose
/authRegister, login, refresh, OTP, password reset
/usersProfiles, roles, admin user management
/booksCatalogue, uploads, publishing workflow
/authors, /publishers, /categories, /tagsCatalogue metadata
/book-accessWhat a reader may open
/orders, /paymentsPurchases, rentals, gateways
/subscriptions, /subscription-plansPlans and active subscriptions
/readingProgress, sessions, highlights
/engagementReviews and comments
/home, /home-sections, /home-bannerApp home screen and admin dashboard
/notificationsIn-app records and push
/legal-pages, /app-versionPolicy pages and the update prompt

16. Database Schema

Bookly uses PostgreSQL with 44 tables. You normally never touch them directly — the backend creates everything on first run — but this section is here for anyone integrating with the data or reviewing the structure before deploying.

You do not need to create these tables by hand. Set DB_SYNC=true for the first run and the backend builds the entire schema from its entity definitions. See section 13, step 4.

The schema file

A complete, ready-to-run SQL file ships with the backend at database/schema.sql. Use it if you prefer to create the schema manually, or to review it up front:

createdb bookly
psql -d bookly -f database/schema.sql

Set DB_SYNC=false in your .env afterwards, so the backend does not try to manage the schema as well.

Conventions

Tables by area

AreaTables
Accounts users, roles, user_roles, user_devices, email_otps
Catalogue books, authors, publishers, categories, tags, book_authors, book_categories, book_publishers, book_tags
Access & commerce book_access, orders, order_items, payment_products, book_payment_products, payment_transactions, payment_gateway_configs, payment_gateway_regions
Subscriptions subscriptions, subscription_plans, subscription_plan_payment_products, subscription_event_logs
Reading reading_progress, reading_sessions, highlights, book_favorites
Engagement reviews, review_votes, comments, comment_likes
Requests audiobook_requests, author_requests
Merchandising home_banners, home_sections, legal_pages, app_version_configs
Notifications notifications, notification_broadcasts, scheduled_notifications
Audit admin_activity_logs

Key tables

users

ColumnTypeNotes
iduuidPrimary key
full_namevarcharRequired
emailvarcharRequired, unique
password_hashvarcharbcrypt. Never returned by the API
refresh_token_hashvarcharHashed refresh token. Never returned by the API
auth_providerenumemail, or a social provider
firebase_uidvarcharSet for Firebase sign-in
subscription_planenumfree or premium
preferred_languagevarcharLocale code
is_activebooleanDeactivated accounts cannot log in

Roles are many-to-many via user_roles → roles, so one account can hold several roles.

books

ColumnTypeNotes
titlejsonbRequired, multilingual
descriptionjsonbMultilingual
file_urlvarcharEPUB location
pdf_urlvarcharPDF, used when there is no EPUB
sample_file_urlvarcharFree preview
cover_image_urlvarcharCover image
dominant_colorvarcharHex colour used by the app's reader theme
statusenumSee the publishing states below
is_free, is_ads_supported, is_subscribable, is_rentable, is_lifetime_purchasablebooleanHow the book may be obtained
base_price, rent_pricenumericLifetime and rental pricing
rent_duration_in_daysintegerRental length

book_access — who may read what

This is the table the reader's entitlement check reads. A row grants one user access to one book.

ColumnNotes
user_id, book_idWho, and which book
access_typefree, subscription or single_buy
granted_atWhen access started
expires_atNULL means permanent; a date means it is a rental
order_id, subscription_idWhat paid for it
is_activeAccess can be revoked without deleting the row

Enum values

These are real PostgreSQL enum types, so the database itself rejects anything outside the list.

TypeValues
Book statusdraft · pending · pending_review · published · scheduled · rejected · archived
Book access typefree · subscription · single_buy
Order kindbook · subscription
Order / transaction statuspending · paid · failed · refunded
Billing cycleweekly · monthly · six_monthly · yearly
Package billing typeone_time · recurring
Purchase typerent · lifetime · subscription_weekly · subscription_monthly · subscription_six_monthly · subscription_yearly
Payment gatewaypiprapay · bkash · nagad · sslcommerz · google_play · apple · stripe · revenuecat
Highlight typehighlight · bookmark · note
Banner typehero · feature · popup
OTP purposeregister · forgot_password · email_verify
Comment statusactive · hidden · flagged · deleted
App version platformandroid · ios

Backing up

# Back up
pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" > bookly-backup.sql

# Restore
psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -f bookly-backup.sql
Remember the files too. The database stores only the paths to covers and book files. If you are on local disk storage, back up the uploads/ folder alongside the database or your restored catalogue will have no files. With Cloudflare R2 or Bunny the files already live in your bucket.

17. FAQ & Common Build Errors

The problems buyers hit most often, and what fixes them. If your issue is not here, please get in touch — we are happy to help.

Before anything else

Most build failures come from a version mismatch. Confirm these first:

Flutter app — build errors

Unsupported class file major version / Gradle fails immediately

Your Java version is wrong. Install Java 21.0.9 and point Android Studio at it in Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JDK. Then:

flutter clean
flutter pub get

Execution failed for task ':app:processDebugGoogleServices'

google-services.json is missing, or its package name does not match your app. Download a fresh one from your Firebase project and place it at android/app/google-services.json. The package_name inside it must equal the applicationId in android/app/build.gradle.kts. See section 4.

Could not resolve all files for configuration / Gradle download stalls

Usually a network or cache problem. Clear the caches and retry:

cd android
./gradlew clean
cd ..
flutter clean
flutter pub get

If it still fails, delete the Gradle cache (~/.gradle/caches) and let it download again.

Version solving failed / dependency conflicts after flutter pub get

flutter clean
rm pubspec.lock
flutter pub get

The lock file ships with the package so you get the exact versions we tested. Only delete it if you have deliberately changed a dependency.

Minimum supported Gradle version / Android Gradle Plugin errors

Open the project with the bundled Gradle wrapper rather than a system Gradle. Always open the project root in Android Studio, not the android/ folder on its own.

iOS: CocoaPods errors or missing pods

cd ios
pod repo update
pod install --repo-update
cd ..
flutter clean
flutter pub get

Requires CocoaPods 1.16.2 or newer (pod --version). On Apple Silicon, if pod install fails try arch -x86_64 pod install.

iOS: Signing for "Runner" requires a development team

Open ios/Runner.xcworkspace in Xcode, select the Runner target, go to Signing & Capabilities and choose your Apple developer team. See section 12.

The app builds but shows a blank screen or no books

It cannot reach the backend. Check in this order:

Backend

Sign-up fails with 400 "SMTP not configured"

Registration sends an email one-time code, so it needs a working mail server. Set MAIL_ENABLED=true and fill in the SMTP_* values (section 13, step 7). Existing accounts can still log in without SMTP — only new sign-ups are blocked.

No tables were created

DB_SYNC=true is required on the first run. Alternatively load database/schema.sql by hand (section 16).

"Superadmin bootstrap complete" never appears, and I cannot log in

The first admin is only created when the database has no users at all. If you have already created one, the bootstrap is skipped by design. Either use the original account, or start from an empty database.

Warning at startup: "Firebase is not configured"

Expected when you have not set up push notifications. The server runs normally; push and Firebase sign-in are simply disabled until you add the FIREBASE_* values.

Error: listen EADDRINUSE — port already in use

# See what is using the port
lsof -i :5008
# Stop it, or pick another port in .env
PORT=5009

Files keep landing in uploads/ instead of Cloudflare R2

R2 needs all of CLOUDFLARE_R2_ACCESS_KEY_ID, CLOUDFLARE_R2_SECRET_ACCESS_KEY, CLOUDFLARE_R2_BUCKET, CLOUDFLARE_R2_ENDPOINT and CLOUDFLARE_R2_PUBLIC_URL. If one is missing it falls back to local disk without an error message. CLOUDFLARE_R2_PUBLIC_URL is the one people forget.

413 when uploading a book

Raise client_max_body_size in nginx to at least your HTTP_BODY_LIMIT (section 13, step 9), then reload nginx.

429 Too Many Requests when logging in

The brute-force limiter. Wait a minute, or raise AUTH_RATE_LIMIT_MAX. If all your users are being limited at once, nginx is not forwarding X-Forwarded-For, so everyone looks like one visitor — see section 13, step 9.

PM2 starts, then the app immediately stops

pm2 logs bookly_backend --lines 50

Usually a missing .env in the folder named by cwd in prod.config.js, or wrong database credentials.

Admin panel

Every page is empty and the browser console shows 404s

NEXT_PUBLIC_API_URL is missing the /api/v1 ending. It must be, for example, https://api.your-domain.com/api/v1. Fix it and run npm run build again.

Covers and images do not load

Either NEXT_PUBLIC_IMAGE_URL wrongly ends in /uploads (the paths already contain it), or your CDN hostname is not allowed — add it to NEXT_PUBLIC_EXTRA_IMAGE_HOSTS.

CORS errors in the console

Add the admin panel's address to CORS_ORIGINS in the backend .env, then restart the backend.

I changed .env but nothing happened

NEXT_PUBLIC_* values are compiled into the browser bundle. Run npm run build and restart.

General questions

Do I need the backend to run the app?

Yes. The app is a client for the Bookly API — the catalogue, accounts, purchases and reading progress all live there.

Can I host the backend and admin panel on the same server?

Yes. Run them on different ports (5008 and 4000 by default) and put nginx in front of both.

Can I use MySQL instead of PostgreSQL?

No. The schema relies on PostgreSQL-specific features such as jsonb for multilingual fields and native enum types.

Which languages are supported?

English, Bangla, Arabic and Hindi out of the box, including right-to-left layout for Arabic. Set the active list in APP_LOCALES (backend) and NEXT_PUBLIC_APP_LOCALES (admin panel) — they must match. Adding another language is covered in section 9.

How do I add a book?

In the admin panel: Books → Add Book. Upload the EPUB or PDF and a cover, fill in the details for each language, set pricing, then publish. Draft books do not appear in the app.

Do I have to use PipraPay?

No. It is off by default. The payment layer is provider-based, and Google Play / Apple in-app purchases are supported separately.

Where are uploaded files stored?

Cloudflare R2, Bunny.net, or the local uploads/ folder — whichever is configured first. See section 13, step 6.

How do I stop people sharing book download links?

Set PROTECT_BOOK_FILES=true. Links then carry an expiring signature and cannot be reused elsewhere. Your apps need no changes. Note that paid books are already withheld from readers who have not bought them.

Can I remove AdMob or in-app purchases?

Yes — both are configurable. See sections 10 and 11.

Still stuck? Send us the exact error text plus the output of flutter doctor -v (app issues) or pm2 logs (backend issues), and we will get you moving. Contact details are in section 18.

18. Support

If you need help, reach out using any of the channels below.

19. Changelog

Version 1.1.1 – 19 Sep 2026

Version 1.1.0 – 14 Mar 2026

Version 1.0.0 – 3 Mar 2026