πΊπΈ English | π§π· PortuguΓͺs | πͺπΈ EspaΓ±ol
About β’ Architecture β’ Features β’ Stack β’ Structure β’ Routes β’ Getting Started β’ Configuration β’ Emails β’ Cloudflare Tunnel β’ Demo Mode β’ Testing β’ Deployment β’ Security β’ Troubleshooting β’ License β’ Related Projects
β οΈ Disclaimer: Smart Option is a demonstration project released exclusively for learning, educational purposes, and portfolio presentation. Originally developed to meet the requirements of a real freelance project that was never deployed to production, it was later expanded into a case study showcasing software architecture, financial integrations, automation, and engineering best practices. Under no circumstances should it be used, adapted, or interpreted as a tool for generating real financial returns or for real-world investment activities.
Smart Option is an automated investment platform that combines the convenience of a Telegram bot with a dedicated admin panel for managing the operation. Integrated with PIX through Asaas, the platform lets users make deposits, purchase yield plans, track their earnings and financial transactions, manage an affiliate network with up to three commission tiers, and request withdrawals β all quickly and without ever leaving Telegram.
This repository holds the Smart Option backend, which owns the platform's entire business logic. Beyond serving the REST API consumed by the admin panel, it runs the Telegram bot, authentication, financial integrations, transaction processing, yield rules, the affiliate system, notifications, and other internal processes. Built with Node.js and TypeScript, it uses MySQL with Drizzle ORM for persistence, Redis for caching and session management, and BullMQ for asynchronous job processing β a modern, scalable architecture ready for production environments.
Smart Option follows Clean Architecture principles, splitting responsibilities into independent modules to make the codebase easier to evolve, test, and maintain. Business logic, infrastructure, and interfaces stay decoupled, so external integrations can be swapped without touching the domain rules.
config/ β Configuration, environment variables, and demo mode
shared/ β Shared building blocks (errors, logger, validation, cache, security)
infrastructure/ β Database, Redis, BullMQ, OpenAPI, and HTTP infrastructure
interfaces/ β DTOs, validation, and HTTP routes
payments/ β Payment gateway integration
notifications/ β Email delivery system
wallet/ β Centralized control of financial transactions
services/ β Platform business rules
server/ β Application bootstrap, middleware, cron jobs, and schedulers
bot/ β Telegram flows, sessions, and interaction
A few decisions were made to keep the application secure, decoupled, and ready to evolve.
-
WalletService centralizes every financial transaction on the platform. No other module changes balances directly, which guarantees consistency, traceability, and idempotency.
-
The payments module depends only on the
PaymentProviderinterface, so the current gateway (Asaas) can be replaced by any other without touching business rules. -
The notifications module follows the same principle through the
EmailProviderinterface, allowing you to switch between Resend, SMTP, or a demo-specific provider purely through configuration. -
Asaas webhooks are processed asynchronously with BullMQ, which improves throughput and brings automatic retries and event deduplication.
-
Demo mode has a single source of configuration (
config/demo.ts), responsible for enabling demo-only features and blocking irreversible operations without leaking into the rest of the application. -
The plan catalog is fully manageable from the admin panel, while the system's default plans stay protected to preserve critical business rules.
-
The Telegram bot keeps per-user sessions in Redis and routes every conversation flow through a single dispatcher, making navigation predictable and maintenance simpler.
-
Seeders stay decoupled from the commands that run them. The same plan catalog is reused during initial setup, manual plan updates, and demo environment restores β no duplicated data, one single source of truth.
The features below are grouped by the two modules that make up Smart Option: the Telegram bot, focused on the end-user experience, and the admin panel API, focused on managing and operating the platform.
The bot brings the entire user journey into a single interface:
- Full sign-up with CPF validation, address, PIX key, and email verification.
- Secure authentication with isolated per-user sessions.
- PIX deposits through Asaas, with QR code, copy-and-paste code, and automatic webhook confirmation.
- Purchase of automatic plans directly from the bot.
- Requests for manual plans, routed to the admin team for review.
- PIX withdrawal requests, subject to admin approval.
- Internal transfers between users, using email as the identifier.
- Balance, statement, earnings, and complete transaction history.
- Management of an affiliate network with up to three commission tiers.
- Automatic yield processing based on the purchased plan.
- Built-in support channel for user assistance.
The API exposes everything needed to run the platform from the admin panel:
- JWT-based authentication with rotating refresh tokens and reuse detection.
- Abuse protection through distributed rate limiting backed by Redis.
- Admin dashboard with KPIs, charts, period-over-period comparisons, and consolidated platform metrics.
- Complete user management, including search, filters, audit trails, and administrative adjustments.
- Admin team management: creating members, editing name, email, and password, reassigning roles, and deactivating accounts β all recorded in the audit trail.
- Approval and management of deposits, withdrawals, subscriptions, and financial requests.
- Full financial audit, with traceability for every transaction on the platform.
- Audit log: records every action taken in the panel, including author, date, time, and the history of the changes.
- Affiliate structure management and per-user network tracking.
- Complete administration of the plan catalog (AUTO and MANUAL), with safeguards for critical system resources.
- Optional demo mode, with guest sign-in, blocking of irreversible operations, and automatic environment restore.
- Interactive API documentation via Swagger/OpenAPI.
Beyond the core features, the project also includes:
- Architecture based on Clean Architecture and SOLID principles.
- Asynchronous processing with BullMQ.
- Distributed caching and session management with Redis.
- Financial integration via Asaas.
- RBAC-based permission system.
- Full audit trail for financial operations.
- A demo environment fully isolated from production.
- Technical documentation and a versioned API.
| Category | Technologies |
|---|---|
| Language & Runtime | Node.js 24, TypeScript 5.9 |
| API & HTTP | Express 4, Helmet, CORS (allowlist), express-rate-limit (Redis store) |
| Database | MySQL 8.4, Drizzle ORM, drizzle-kit |
| Cache & Queues | Redis 7, BullMQ |
| Bot | node-telegram-bot-api |
| Payments | Asaas (PIX, transfers, and webhooks) |
| Authentication | JWT (jsonwebtoken), bcryptjs |
| Validation | Zod |
| Logging & Observability | Pino, pino-http, x-request-id |
| Testing | Vitest, Supertest |
| Infrastructure | Docker (multi-stage), Docker Compose, Caddy (automatic TLS via Let's Encrypt) |
During development, the project relies on a few tools to simplify environment setup and enable external integrations without exposing the local machine directly.
| Tool | Purpose |
|---|---|
| Cloudflare Tunnel | Securely exposes the local environment for webhook testing and external integrations. |
| Docker Compose | Orchestrates the development services. |
| Swagger / OpenAPI | REST API documentation and testing. |
src/
ββ config/ # Application and environment configuration
ββ shared/ # Errors, logger, validation, and shared building blocks
ββ infrastructure/ # Database, Redis, queues, middleware, and OpenAPI
ββ interfaces/ # DTOs and HTTP routes
ββ payments/ # Payment gateway abstraction and implementation
ββ notifications/ # Email delivery abstraction and implementation
ββ wallet/ # Ledger and balance management
ββ services/ # Business rules consumed by the API and the bot
ββ server/ # API bootstrap, routes, middleware, and scheduled jobs
ββ bot/ # Telegram dispatcher, sessions, flows, and interfaces
cloudflared/
ββ config.yml # Development tunnel configuration
scripts/
ββ lib.* # Shared functions
ββ start-dev.* # Development environment
ββ start-tunnel.* # Cloudflare Tunnel
ββ run-platform.js # Windows, Linux, and macOS compatibility
The API is organized into modules and documented with Swagger/OpenAPI at /api/docs.
Every protected route requires a JWT access token in the header:
Authorization: Bearer <accessToken>Public routes:
/health/api/health/api/docs/api/auth/*/email/verify/:token/api/webhooks/asaas
Note
This section covers the API's main endpoints. Full documentation, with parameters, request examples, and responses, is available at
GET /api/docs.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Checks whether the application is running (liveness). |
| GET | /api/health |
Checks the availability of the application, MySQL, and Redis (readiness). |
| GET | /api/docs |
Interactive API documentation (Swagger/OpenAPI). |
Handles admin panel authentication, session management, token renewal, and demo mode access.
| Method | Endpoint | Description |
|---|---|---|
| POST | / |
Authenticates an admin. |
| POST | /demo-login |
Creates a temporary demo session, no credentials required. Available only when APP_DEMO=true. |
| POST | /refresh |
Issues a new access token from a valid refresh token. |
| POST | /logout |
Revokes the current session's refresh token. |
| POST | /token |
Validates an access token and reports whether the session is in demo mode. |
Management of panel admins and of the users registered through the Telegram bot.
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
Lists the registered admins. |
| PATCH | /update-user |
Updates the authenticated admin's details. |
| PATCH | /update-pass |
Changes the authenticated admin's password. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /users-bot/:search |
Searches users by free-text term. |
| POST | /users-bot |
Searches users with advanced filters. |
| POST | /user-bot |
Creates a new user. |
| GET | /user-bot/:id |
Retrieves a user's details. |
| PATCH | /user-bot |
Updates a user's details. |
| DELETE | /user-bot/:id |
Deletes a user. |
| PUT | /user-bot/:id/:status |
Activates or deactivates a user. |
| POST | /transf-user-admin |
Performs a manual balance adjustment with an audit record. |
Endpoints behind the strategic metrics shown in the admin panel.
| Method | Endpoint | Description |
|---|---|---|
| GET | /summary |
Returns the dashboard's main metrics, including KPIs, charts, approvals for the day, and recent activity, with period filters and Redis caching. |
| GET | /plans |
Lists the plans available for display in the dashboard. |
Complete management of the platform's plan catalog.
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
Lists plans with pagination, filters, and sorting. |
| GET | /:id |
Retrieves a plan's details. |
| POST | / |
Creates a new plan. |
| PATCH | /:id |
Updates an existing plan. |
| DELETE | /:id |
Deletes a plan. System plans and plans with active subscribers can't be deleted. |
Two complementary trails: the platform's financial transactions and the administrative actions β who changed what. Every change to the team, roles, bot users, blocks, balance adjustments, withdrawal responses, and support resolutions is recorded with author, timestamp, and the state before and after. Password resets are flagged as such, without ever storing the password or its hash.
| Method | Endpoint | Description |
|---|---|---|
| POST | / |
Returns the consolidated financial transaction history, with filters, pagination, sorting, and advanced search. |
| POST | /actions |
Returns the administrative action history β who changed what in the panel, with the state before and after each change. |
Lookup of the affiliate hierarchy tied to each user.
| Method | Endpoint | Description |
|---|---|---|
| POST | /:id |
Returns the complete affiliate network for a user. |
Management of deposits, withdrawals, subscriptions, support, and other operational requests.
| Method | Endpoint | Description |
|---|---|---|
| GET | /extract/:id |
Retrieves a user's financial statement. |
| POST | /extract/:id |
Retrieves the statement using advanced filters. |
| POST | /withdrawal/:id |
Lists withdrawal requests. |
| POST | /deposit/:id |
Lists deposits. |
| POST | /subscription/:id |
Lists plan subscription requests. |
| POST | /support/:id |
Lists support tickets. |
| POST | /res-withdrawal |
Approves or rejects a withdrawal request. |
| PATCH | /was-read/:id/:status |
Updates the read status of a support ticket. |
| GET | /pendencies |
Returns the platform's total pending items. |
Endpoints used by external integrations and resources available without authentication.
| Method | Endpoint | Description |
|---|---|---|
| GET | /email/verify/:token |
Confirms a user's email address. |
| POST | /api/webhooks/asaas |
Receives Asaas events for payments, charges, and PIX transfers. |
This section walks through setting up the Smart Option local development environment.
- Docker and Docker Compose
- Node.js 24+ (only needed to run the API directly on the host)
- A Telegram bot token created with BotFather
- An Asaas Sandbox account with an API key
- Optional:
cloudflaredinstalled and authenticated to receive webhooks locally (see the Cloudflare Tunnel section)
Clone the repository and set up the environment:
git clone <repository-url>
cd smart-option
cp .env.development.example .envEdit the .env file and set at least:
SECRET_KEYJWT_REFRESH_SECRETBOT_TOKENBOT_USERASAAS_API_KEY- email settings (Resend or SMTP)
To generate the keys:
Linux/macOS
openssl rand -hex 32Windows (PowerShell)
-join ((1..32 | % { '{0:x2}' -f (Get-Random -Min 0 -Max 256) }))Then simply run:
npm run dev:fullThis command orchestrates the entire development environment for you:
- starts MySQL, Redis, and the API in Docker containers;
- enables hot reload through a bind mount;
- waits for every service to become available;
- sets up the Cloudflare Tunnel (when installed);
- validates the public endpoints used by Asaas;
- prints the public URL, ready to register as a webhook.
Note
If you're not using the Cloudflare Tunnel, just run:
npm run docker:upThe bot keeps working normally over long polling, but deposits, subscriptions, and withdrawals won't be confirmed automatically, since they depend on Asaas webhooks.
You can also run the API directly on the host, keeping only MySQL and Redis in containers.
git clone <repository-url>
cd smart-option
npm install
cp .env.development.example .envStart the infrastructure:
npm run docker:upThen run:
npm run db:migrate
npm run db:seed
npm run devThe API will be available at:
http://localhost:<APP_PORT>
If you want to expose the application to receive webhooks, run the tunnel in a separate terminal:
npm run tunnel| Script | Description |
|---|---|
npm run dev |
Runs the API and the bot directly on the host with hot reload. |
npm run dev:full |
Starts the full Docker environment, sets up the Cloudflare Tunnel, and validates the infrastructure. |
npm run docker:up |
Starts MySQL, Redis, and the API in Docker. |
npm run docker:down |
Removes the development containers. |
npm run tunnel |
Starts the Cloudflare Tunnel only. |
npm run build |
Compiles the application for production. |
npm start |
Runs the compiled application. |
npm test |
Runs the test suite. |
npm run test:watch |
Runs the tests in watch mode. |
npm run test:coverage |
Generates the test coverage report. |
npm run lint |
Lints the code with ESLint. |
npm run lint:fix |
Auto-fixes issues found by ESLint. |
npm run format |
Formats the code with Prettier. |
npm run format:check |
Checks whether the code is properly formatted. |
npm run db:generate |
Generates migrations with Drizzle Kit. |
npm run db:migrate |
Applies pending migrations. |
npm run db:studio |
Opens Drizzle Studio. |
npm run db:seed |
Seeds the database with the initial data (plan catalog + admin account). |
npm run db:backfill-wallets |
Backfills the wallet ledger from the legacy balance history. |
npm run plans:seed |
Ensures the default plan catalog exists (idempotent, independent of demo mode). |
npm run demo:seed |
Wipes and regenerates the demo data (destructive β equivalent to demo:reset). Requires APP_DEMO=true. |
npm run demo:reset |
Restores the demo environment to its initial state (destructive). Requires APP_DEMO=true. |
The application uses .env files for all environment configuration. There is no behavioral difference between development and production in the code β everything is driven by environment variables.
The available example files are:
| File | Purpose | Usage |
|---|---|---|
| .env.development.example | Local development (Docker, Cloudflare Tunnel, and Asaas Sandbox) | Copy to .env on your machine |
| .env.production.example | Production environment (Asaas Production and Caddy) | Copy to .env on the VPS |
Every variable the application uses is defined in those files and validated at startup by src/config/env.ts with Zod.
If a required variable is missing or invalid, the application stops during startup (fail-fast) with an error message pointing at exactly what's wrong.
Variables with a default value can be omitted; the ones without a default are required for the application to run.
| Category | Main variables |
|---|---|
| Application | NODE_ENV, APP_PORT, API_BASE_PATH |
| Reverse proxy (production) | DOMAIN, ACME_EMAIL |
| Database | DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_DATABASE |
| Redis | REDIS_URL, REDIS_PORT |
| Authentication | SECRET_KEY, JWT_ACCESS_EXPIRES_IN, JWT_REFRESH_SECRET, JWT_REFRESH_EXPIRES_IN |
| Telegram | BOT_TOKEN, BOT_USER |
| Logging | LOG_LEVEL |
| CORS & rate limiting | CORS_ALLOWED_ORIGINS, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX |
| Asaas | ASAAS_ENV, ASAAS_API_KEY, ASAAS_BASE_URL, ASAAS_WEBHOOK_TOKEN |
| Cloudflare Tunnel | CF_TUNNEL_ID, CF_TUNNEL_TOKEN, CF_TUNNEL_DOMAIN, CF_TUNNEL_HOST |
EMAIL_TYPE, RESEND_API_KEY, MAIL_FROM_NAME, MAIL_FROM_ADDRESS, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD |
|
| Demo mode | APP_DEMO, AUTO_RESET, AUTO_RESET_INTERVAL |
Important
Values starting with
$(such asASAAS_API_KEY) must be wrapped in single quotes in the.envfile:ASAAS_API_KEY='$aact_...'See the Troubleshooting section for details.
Email delivery lives in the src/notifications module and follows the same architectural pattern as payments/: the application depends only on the EmailProvider interface, while the implementation is chosen at runtime.
All communication with email providers goes through notificationService, which handles operations like email verification, password recovery, and deposit notifications β keeping controllers, services, and bot flows decoupled from the underlying technology.
src/notifications/
ββ interfaces/ # Email provider contracts
ββ providers/ # Implementations (Resend and SMTP)
ββ templates/ # Reusable email templates
ββ factory/ # Provider selection based on EMAIL_TYPE
ββ services/ # Facade consumed by the rest of the application
With EMAIL_TYPE=resend (the default), the application uses Resend's official HTTP API.
The sender is defined by:
MAIL_FROM_NAME=Smart Option
MAIL_FROM_ADDRESS=smart-option@example.urlWhich results in:
From: Smart Option <smart-option@example.url>
Required variables:
RESEND_API_KEYMAIL_FROM_NAMEMAIL_FROM_ADDRESS
With EMAIL_TYPE=smtp, the application uses SMTP through Nodemailer, with TLS support, connection timeout, and automatic retry for transient failures.
Required variables:
SMTP_HOSTSMTP_USERSMTP_PASSWORD
SMTP_PORT defaults to port 465.
Changing providers is done entirely through the environment variable:
EMAIL_TYPE=resendor
EMAIL_TYPE=smtpNo other code changes are needed.
To integrate a new service (Amazon SES, Mailgun, Brevo, SendGrid, etc.):
- implement the
EmailProviderinterface; - register the new provider in
email.factory.ts; - add the required variables to the schema in
src/config/env.ts.
Since the entire application depends only on the EmailProvider interface, no controller, service, or bot flow needs to change.
During development, Asaas needs to reach the API to deliver payment confirmation webhooks. To make that possible, the project uses a Cloudflare Tunnel with a fixed domain, accepting public requests without exposing any ports on the local machine.
The Cloudflare Tunnel is used in development only. In production, the application is published through Caddy with automatic HTTPS.
| System | Command |
|---|---|
| Windows (Chocolatey) | choco install cloudflared |
| Windows (Scoop) | scoop install cloudflared |
| macOS (Homebrew) | brew install cloudflared |
| Linux | https://pkg.cloudflare.com/index.html |
| Manual | https://github.com/cloudflare/cloudflared/releases |
If cloudflared isn't installed, npm run tunnel and npm run dev:full print these instructions automatically.
Before the first run, authenticate the machine against your Cloudflare account:
cloudflared tunnel loginYour browser will open so you can authorize access to the domain used by the project.
This only needs to be done once per machine.
Once authenticated, just run:
npm run dev:fullor
npm run tunnelOn the first run, the project:
- creates a persistent tunnel;
- registers the DNS record on Cloudflare automatically;
- saves the tunnel ID in
.env; - reuses the same configuration on subsequent runs.
No additional manual setup is required.
The file:
cloudflared/config.yml
is used as a template.
At runtime, the project generates the configuration from the .env variables, avoiding duplicated values such as domain, port, and tunnel ID.
After starting the environment:
npm run dev:fullyou'll see a summary like this:
========================================
Smart Option API
Running:
http://localhost:3000
Cloudflare Tunnel:
https://example.url
Webhook URL:
https://example.url/api/webhooks/asaas
========================================
Register the Webhook URL shown above in the Asaas Sandbox dashboard.
When you stop the application (Ctrl + C), the tunnel shuts down too. Docker containers keep running until you stop them with:
npm run docker:downIn the Asaas Sandbox dashboard:
- go to Integrations β Webhooks;
- register the URL printed by
npm run dev:full; - set the same value defined in
ASAAS_WEBHOOK_TOKEN; - send a test event and follow the processing in the application logs.
cloudflared runs directly on the host and forwards requests to the API running in Docker, through the port set in APP_PORT.
This keeps the development environment simple and avoids running an extra container just for the tunnel.
Demo mode turns Smart Option into a public showcase, letting any visitor explore virtually every feature without compromising the application's security or performing real operations.
The whole environment is designed to feel close to production, with realistic data, while preventing any action that could affect external systems, critical information, or real money.
β οΈ Everything described in this section is controlled by theAPP_DEMOvariable. WithAPP_DEMO=false(the default), none of it exists: the guest sign-in route isn't registered, the blocks stay inactive, and any attempt to run a reset command stops immediately.
| Variable | Default | Description |
|---|---|---|
APP_DEMO |
false |
Enables demo mode, including guest sign-in, blocking of critical operations, and the environment restore commands. |
AUTO_RESET |
false |
Enables automatic environment restore. Requires APP_DEMO=true. The server refuses to start if the combination is invalid. |
AUTO_RESET_INTERVAL |
60 | Automatic reset interval in minutes (60, 1440, etc.). Required when AUTO_RESET=true. |
Only explicit values (true, 1, or yes) enable a feature. Anything else leaves it turned off.
When the backend runs in demo mode, the sign-in screen shows an Enter as guest button.
POST /api/auth/demo-loginNo public credentials are exposed.
When this route is used, the backend creates a temporary session on an internal demo account (visitante@demo.local) that can only be authenticated through this endpoint.
The account holds every permission needed to explore the full system. The demo's safety doesn't come from reduced permissions, but from specifically blocking the operations that could cause permanent or external effects.
During the demo, certain actions return HTTP 403 with the message:
This action is disabled in the demo.
The admin panel mirrors this behavior, visually disabling those actions and explaining why to the user.
| Action | Reason |
|---|---|
POST /api/requests/res-withdrawal |
Prevents real PIX transfers through Asaas. |
POST / PATCH / DELETE on /api/staff and /api/roles |
Prevents changes to admin users and system permissions. |
PATCH /api/users/update-user and /update-pass |
Protects the credentials of the shared admin account. |
On top of that, no external integration is executed.
When APP_DEMO=true, email delivery uses a null provider that only records messages in the application logs.
Still fully available:
- bot user management;
- manual balance adjustments;
- plan management;
- simulated financial transactions;
- queries and audits;
- support tickets;
- dashboards, charts, and reports.
npm run demo:resetThis command fully restores the demo environment.
Among the operations it runs:
- clearing the transactional tables;
- recreating the fictional data;
- syncing the plan catalog;
- clearing the dashboard cache;
- rebuilding the affiliate network;
- generating the simulated financial transactions.
Administrative data is preserved.
The staff_users and roles tables are never wiped, so admins keep access to the environment. Likewise, products is synced through an upsert, preserving the identifiers the application relies on internally.
If APP_DEMO=false, the command stops immediately, before any change reaches the database.
To enable automatic restores:
APP_DEMO=true
AUTO_RESET=true
AUTO_RESET_INTERVAL=60The scheduler runs in the same process as the application and skips a cycle if a previous reset is still in progress.
The generator builds a consistent environment for showcasing the platform.
The dataset includes roughly:
- 300 users;
- a three-tier affiliate network;
- active plans;
- deposits;
- earnings;
- commissions;
- withdrawal requests;
- support tickets;
- financial history;
- a complete audit trail.
npm run demo:seedThe command always rebuilds the environment from scratch before generating new data, which avoids piling up fictional records and keeps the scenario predictable.
Right now, demo:seed reuses exactly the same routine as demo:reset, keeping a single data-generation path.
The same guards still apply: both commands only run when APP_DEMO=true.
The demo environment follows two core principles:
-
Deterministic scenario: the shape of the demo stays consistent across restores, with only internal identifiers and dates relative to the run time varying.
-
Financial consistency: every balance stays in sync with the transaction ledger, so dashboards, reports, and audits always show exactly the same numbers.
The project uses Vitest for unit and integration tests.
| Command | Description |
|---|---|
npm test |
Runs the entire test suite. |
npm run test:watch |
Runs the tests in watch mode, re-running automatically on changes. |
npm run test:coverage |
Runs the full suite and generates the code coverage report. |
The tests combine:
- Unit tests, focused on isolated business rules;
- Integration tests, running against real MySQL and Redis instances started by
docker-compose.dev.yml.
The main flows covered include:
- authentication and JWT token renewal;
- WalletService transactions;
- payment and webhook processing;
- affiliate network commission calculation.
Important
To run the full integration suite, MySQL and Redis must be running (
npm run docker:upornpm run dev:full).
Tests run sequentially (fileParallelism: false) to avoid concurrency conflicts in operations that share the same database.
This guide walks through deploying the Smart Option Backend (API + Telegram bot) to a Linux VPS using Docker Compose and Caddy.
- Ubuntu/Debian VPS
- root or sudo access
- a domain pointed at the VPS IP (A record)
- Docker Engine + Docker Compose
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in to apply the docker group
docker --version
docker compose versionOpen ports 80 and 443 on the firewall:
ufw allow 80,443/tcpOnly Caddy is exposed to the internet. The API, MySQL, and Redis stay reachable only through the Docker Compose internal network.
git clone <repository-url> smart-option
cd smart-option
cp .env.production.example .envEdit the .env file with your real production values.
SECRET_KEYJWT_REFRESH_SECRETDB_PASSWORDBOT_TOKENBOT_USERASAAS_API_KEYASAAS_BASE_URLASAAS_WEBHOOK_TOKENCORS_ALLOWED_ORIGINSSMTP_*DOMAINACME_EMAIL
To generate the JWT keys:
openssl rand -hex 32Important
Values starting with
$(such asASAAS_API_KEY) must be wrapped in single quotes:ASAAS_API_KEY='$aact_prod_...'Without them, Docker Compose may interpret the value as another environment variable.
DB_HOSTandREDIS_URLdon't need to change in production β Docker Compose already points them at the internal services.
The .env file is mounted directly into the container and must stay in the project root.
docker compose -f docker-compose.prod.yml up -d mysql redis
docker compose -f docker-compose.prod.yml psWait for both services to report healthy.
docker compose -f docker-compose.prod.yml --profile tools run --rm migrateOn the first deployment, also run:
docker compose -f docker-compose.prod.yml --profile tools run --rm migrate npm run db:seedThe initial seed creates the default products used by the system.
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml psCaddy automatically obtains a valid TLS certificate for the domain set in DOMAIN.
To follow the process:
docker compose -f docker-compose.prod.yml logs -f caddyOnce the certificate is issued, verify the application:
curl https://YOUR_DOMAIN/api/healthIt's also worth checking the application logs:
docker compose -f docker-compose.prod.yml logs -f appNo additional setup is required.
Caddy renews Let's Encrypt certificates automatically before they expire β no cron job, no manual step.
docker compose -f docker-compose.prod.yml logs -f app
docker compose -f docker-compose.prod.yml logs -f caddygit pull
docker compose -f docker-compose.prod.yml up -d --build appIf there are new migrations, run step 4 again before bringing up the new version.
docker compose -f docker-compose.prod.yml stop appThe application shuts down gracefully: it closes the API, the bot, the webhook worker, and the database connections before exiting.
docker compose -f docker-compose.prod.yml exec mysql \
mysqldump -u root -p"$DB_PASSWORD" "$DB_DATABASE" \
| gzip > backup-$(date +%F).sql.gzStore backups outside the VPS.
-
The
appcontainer doesn't support multiple replicas while the bot uses long polling. Running two instances at once triggers a 409 error from Telegram. -
Scaling horizontally would require splitting the API and the bot into separate services and moving Telegram to webhooks.
-
The deployment doesn't include a CI/CD pipeline β updates are performed manually.
-
Caddy uses the official image, without an additional rate limiting module. Abuse protection is still handled by the application itself, through Redis.
The application's security model focuses on protecting authentication, financial transactions, and communication between services.
- Database: every business query uses Drizzle ORM with parameterized queries β no SQL built by concatenating external input.
- Passwords: stored with bcrypt (cost 12). Legacy SHA-1 hashes are migrated to bcrypt on the next successful login (lazy migration).
- Authentication: short-lived JWT access tokens combined with rotating refresh tokens, including reuse detection and automatic revocation of the entire token family.
- Rate limiting: global and auth-specific limits stored in Redis, so behavior stays consistent even across multiple application instances.
- Webhooks: signature validation using constant-time comparison, protecting against timing attacks.
- HTTP security: Helmet, allowlist-based CORS policy, and
trust proxyconfigured exclusively for the infrastructure's reverse proxy. - Infrastructure: only Caddy exposes ports 80 and 443 in production. The API, MySQL, and Redis stay isolated on the Docker Compose internal network, with TLS issued and renewed automatically by Let's Encrypt.
- Secrets: all credentials come exclusively from environment variables β no sensitive data in the source code.
The Cloudflare Tunnel isn't installed or isn't available in your PATH.
See the Cloudflare Tunnel section to install it.
Run:
cloudflared tunnel loginYour browser will open for authentication. Select the account and the zone for the domain used by the project.
If the application reports an "invalid key" even though the variable is set correctly, check that it's wrapped in single quotes.
ASAAS_API_KEY='$aact_hmlg_...'Because Asaas keys start with $, Docker Compose may treat that character as an environment variable when reading the .env file.
Check the service logs:
docker compose -f docker-compose.dev.yml logs mysql redisOn the very first startup, MySQL may take a few extra seconds to finish bootstrapping.
Another API instance is already using the port set in APP_PORT.
Stop the previous instance:
npm run docker:downor kill the process holding the port manually.
npm run dev:fullautomatically reuses an already-running API whenever possible.
terminated by other getUpdates request
The bot uses long polling, which allows only one instance per BOT_TOKEN.
Make sure no other application (development or production) is running with the same token at the same time.
Check that the authenticated account has access to the domain zone used by the project:
cloudflared tunnel loginIf the record isn't created automatically, you can add it manually from the Cloudflare dashboard.
This project is distributed under the Smart Option Source Available License (SSAL).
You may:
- study the source code;
- fork the repository for educational purposes;
- use parts of the implementation as a learning reference.
You may not:
- use this project for commercial purposes;
- offer it as a product or service;
- build investment platforms, multi-level marketing (MLM), HYIP, Ponzi schemes, financial pyramids, gambling, or any similar financial service on top of this code.
See the LICENSE file for the full terms.
Smart Option was built as an ecosystem of independent applications, each with a clear responsibility. Splitting it across repositories keeps things organized, makes parallel development easier, and results in a more modular, scalable architecture.
| Project | Description | Repository |
|---|---|---|
| π Landing Page | The official Smart Option landing page, built to introduce the platform, what sets it apart, and the experience it offers users. | https://github.com/issagomesdev/smart-option-page |
| π Admin Panel (Frontend) | The administrative interface for managing the Smart Option platform. | https://github.com/issagomesdev/smart-option-admin |