Version: 1.0.0
Runtime: Bun >= 1.2.5
Framework: ElysiaJS
Database: MongoDB 8
Cache: Redis 6.2
| Tool | Version | Notes |
|---|---|---|
| Bun | >= 1.2.5 | Primary runtime & package manager |
| Docker Desktop | Latest | For MongoDB & Redis containers |
| Git | Any | Source control |
| Variable | Description | Example |
|---|---|---|
PORT | HTTP server port | 33033 |
NODE_ENV | Runtime environment | development |
API_KEY | JWT signing secret | your-secret-key |
ALLOWED_API_KEY | Static API key for machine-to-machine clients | sha256-hash-value |
IP_ALLOWED | Comma-separated IP whitelist for /get-proxy and /webhook | 127.0.0.1,10.0.0.5 |
DB_MONGO_HOST | MongoDB host (localhost for local, proxy_service_mongodb inside Docker) | localhost |
DB_MONGO_PORT | MongoDB port | 27017 |
DB_MONGO_USERNAME | MongoDB root username | root |
DB_MONGO_PASSWORD | MongoDB root password | secret |
DB_MONGO_DATABASE | Database name | proxy-tool-mongodb |
PROXY_USE_MAX | Max concurrent leases per single proxy | 3 |
WEBHOOK_RETURN_PROXY | Base URL consumers use to return a proxy | http://127.0.0.1:33034/webhook/return-proxy |
KIOT_API_KEY | KiotProxy API token (optional integration) | kiot-token |
API_KIOT_PROXY_PACKAGE | KiotProxy package list endpoint | https://api.kiotproxy.com/api/public/packages |
API_KIOT_PROXY_KEY | KiotProxy key list endpoint | https://api.kiotproxy.com/api/public/keys |
KIOT_PROXY_ROTATE_URL | KiotProxy rotation URL base | https://api.kiotproxy.com/api/v1/proxies/new?key= |
ZING_PROXY_TOKEN | ZingProxy auth token (optional integration) | zing-token |
API_ZING_PROXY_ALL | ZingProxy active proxies endpoint | https://api.zingproxy.com/proxy/get-all-active-proxies |
LOG_LEVEL | Log verbosity: error, warn, info, debug | info |
LOG_WRITE_TO_FILE | Write logs to storage/log/ | true |
RUNTIME | Runtime hint for AOT optimization | bun |
localhost:33027localhost:33025.env uses:DB_MONGO_HOST=localhost
REDIS_HOST=localhosthttp://localhost:33033 (or PORT value in .env).http://localhost:33033http://localhost:33033/swaggerlocalhost:4000/debugger--watch. Any file change restarts the server automatically.| Script | Command | Description |
|---|---|---|
dev | bun run dev | Hot-reload dev server + debugger |
build | bun run build | Bundle to ./dist |
compile | bun run compile | Standalone binary ./dist/main |
clean | bun run clean | Remove ./dist |
format | bun run format | Prettier |
lint | bun run lint | OXLint |
token field from the response as Authorization: Bearer <token> on all protected routes.Database type: MongoDB 8
ODM: Mongoose + Typegoose (decorator-based class definitions)
All collections usetimestamps: true—createdAtandupdatedAtare auto-managed by Mongoose.
proxies| Field | Type | Required | Default | Description |
|---|---|---|---|---|
_id | string (ObjectId hex) | YES | auto-generated | Unique document identifier |
ip | string | YES | — | Current IP/hostname of the proxy server (e.g. 123.45.67.89). Updated after each rotation |
port | number | YES | — | HTTP port of the proxy server (e.g. 8080). Updated after each rotation |
username | string | YES | — | Auth username for the proxy. Updated after each rotation |
password | string | YES | — | Auth password for the proxy. Updated after each rotation |
area | "global" or "vn" | YES | — | Geographic scope. "vn" = Vietnam-specific proxies; "global" = international |
rotate_url | string (URI) | YES | — | URL to call to rotate (refresh) this proxy's IP. Provider-specific endpoint |
rotate_time | number | — | 60 | Rotation interval in seconds between automatic rotations |
auth_token | string or null | — | null | Bearer token sent in Authorization header when calling rotate_url (if provider requires it) |
ip_public | string or null | — | null | Publicly visible IP after the last successful rotation, verified via api.ipify.org |
is_active | boolean | — | true | Whether proxy is enabled and eligible for lease assignment |
is_error | boolean | — | false | true if the last rotation or IP verification failed |
is_rotating | boolean | — | false | Concurrency lock. Set to true while the cron is rotating this proxy. Prevents duplicate rotations |
disable_auto_rotate | boolean | — | false | If true, cron skips the rotation step and only verifies the current IP |
next_rotate | Date | — | now + 60s | Timestamp when proxy becomes eligible for the next rotation cycle |
last_used_at | Date or null | — | null | Timestamp when this proxy was last leased to a consumer |
expired_at | Date or null | — | null | Subscription expiry from the provider. Set during import |
in_use | number | — | 0 | Number of currently active leases. Incremented on GET /api/get-proxy, decremented via return webhook |
used_count | number | — | 0 | Total lease count in the current rotation cycle. Resets to 0 after a successful rotation |
proxy_used | ProxyUsed[] | — | [] | Embedded array of active lease records. Cleared on rotation |
webhook_url | string or null | — | null | Auto-set when leased: {WEBHOOK_RETURN_PROXY}/{id}?key={key}. Consumer must call this URL to release the proxy |
call_webhook_count | number | — | 0 | Counter for failed outbound webhook call attempts |
status | number (enum) | — | 0 (PENDING) | Internal status: 0=PENDING, 1=PROCESSING, 2=COMPLETED |
status_code | number | — | — | HTTP status code from the last rotation API call |
response | string | — | — | Raw response body from the last rotation call (for debugging) |
region | string | — | "" | Geographic region label from provider (e.g. "HCM", "HN") |
package_name | string | YES | — | Denormalized display name from the proxy_packages collection |
proxy_package_id | string (ref → proxy_packages._id) | YES | — | Foreign key to the parent package |
created_by | string (ref → users._id) | — | — | ID of the user who created this record |
updated_by | string (ref → users._id) | — | — | ID of the user who last updated this record |
createdAt | Date | auto | — | Mongoose-managed creation timestamp |
updatedAt | Date | auto | — | Mongoose-managed last-update timestamp |
proxy_used[] (ProxyUsed)| Field | Type | Description |
|---|---|---|
ip | string | IP address of the consumer holding this lease |
key | string | Unique lease key (ObjectId hex). Required to release the proxy via the webhook |
time | Date | Timestamp when the lease was granted |
proxy_packages| Field | Type | Required | Default | Description |
|---|---|---|---|---|
_id | string (ObjectId hex) | YES | auto-generated | Unique identifier |
code | string | YES (unique) | — | Business key for the package. Convention: KiotProxy_<provider_code> for KiotProxy; ZingProxy_Pack for ZingProxy |
name | string | YES | — | Human-readable package name (e.g. "4G VN Tháng") |
price | number | YES | — | Monthly price in VND or provider currency |
note | string | — | "" | Optional notes or description |
proxy_provider_id | string (ref → proxy_providers._id) | YES | — | Foreign key to the parent provider |
is_active | boolean | — | true | Package is active and visible |
created_by | string (ref → users._id) | — | — | ID of the user who created this record |
updated_by | string (ref → users._id) | — | — | ID of the user who last updated this record |
createdAt | Date | auto | — | Mongoose-managed creation timestamp |
updatedAt | Date | auto | — | Mongoose-managed last-update timestamp |
proxy_providers| Field | Type | Required | Default | Description |
|---|---|---|---|---|
_id | string (ObjectId hex) | YES | auto-generated | Unique identifier |
name | string | YES (unique) | — | Provider display name (e.g. "KiotProxy", "ZingProxy") |
account | string | YES (unique) | — | Account name or username used on the provider's platform |
domain | string | YES (unique) | — | Provider website domain (e.g. "https://kiotproxy.com") |
is_active | boolean | — | true | Provider is active and usable |
created_by | string (ref → users._id) | — | — | ID of the user who created this record |
updated_by | string (ref → users._id) | — | — | ID of the user who last updated this record |
createdAt | Date | auto | — | Mongoose-managed creation timestamp |
updatedAt | Date | auto | — | Mongoose-managed last-update timestamp |
users| Field | Type | Required | Default | Description |
|---|---|---|---|---|
_id | string (ObjectId hex) | YES | auto-generated | Unique identifier |
email | string | YES (unique) | — | Email address used as login credential. Validated by regex |
username | string | YES (unique) | — | Display name |
password | string | YES | — | bcrypt-hashed password (cost factor = 4). Never stored in plaintext |
createdAt | Date | auto | — | Mongoose-managed creation timestamp |
updatedAt | Date | auto | — | Mongoose-managed last-update timestamp |
users._id
└─> created_by / updated_by (referenced across all collections)
proxy_providers._id
└─> proxy_packages.proxy_provider_id
proxy_packages._id
└─> proxies.proxy_package_id
proxies._id
└─> proxy_used[] (embedded array — active lease records)┌─────────────────────────────────────────────────────────────────┐
│ Consumer Services │
│ (other microservices that need a proxy) │
└──────────────────┬──────────────────────────┬───────────────────┘
│ │
GET /api/get-proxy?area= POST /webhook/return-proxy/:id?key=
(API Key + IP whitelist) (return lease when done)
│ │
v v
┌─────────────────────────────────────────────────────────────────┐
│ Proxy Service Tool (ElysiaJS / Bun) │
│ PORT: 33033 │
│ │
│ ┌───────────────┐ ┌──────────────────────┐ ┌────────────┐ │
│ │ Auth Routes │ │ Admin Routes │ │ Public │ │
│ │ POST /login │ │ /api/proxies │ │ API │ │
│ │ GET /me │ │ /api/proxy_packages │ │ │ │
│ │ POST /register│ │ /api/proxy_providers │ │ /api/ │ │
│ │ │ │ /api/users │ │ get-proxy │ │
│ │ (no auth) │ │ │ │ │ │
│ └───────────────┘ │ Auth: JWT Bearer │ │ /webhook/ │ │
│ └──────────────────────┘ │ return- │ │
│ │ proxy/:id │ │
│ │ │ │
│ │ Auth: API │ │
│ │ Key + IP │ │
│ └────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Background Cron Jobs │ │
│ │ proxy-cron-rotate (every 10s) │ │
│ │ GetManyCanRotate(20) -> rotateProxy() -> checkIP() │ │
│ │ -> save new ip/port/ip_public │ │
│ │ zing-proxy-cron (every 12h) │ │
│ │ -> fetch ZingProxy active list -> auto-import │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
┌──────────────┐ ┌──────────────┐
│ MongoDB 8 │ │ Redis 6.2 │
│ Port: 27017 │ │ Port: 6379 │
│ (datastore) │ │ (reserved) │
└──────────────┘ └──────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ External Proxy Providers │
│ ┌───────────────────────┐ ┌───────────────────────────────┐ │
│ │ KiotProxy │ │ ZingProxy │ │
│ │ api.kiotproxy.com │ │ api.zingproxy.com │ │
│ │ - List packages │ │ - List active proxies │ │
│ │ - List keys │ │ - Rotate via URL │ │
│ │ - Rotate via URL │ │ │ │
│ └───────────────────────┘ └───────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘| Layer | Used By | Mechanism | Required Header |
|---|---|---|---|
| JWT Bearer | Admin users (Swagger, Frontend) | JWT signed with API_KEY, expires in 14 days | Authorization: Bearer <token> |
| Static API Key | Machine-to-machine admin clients | Raw key matched against ALLOWED_API_KEY env var | Authorization: <allowed_api_key> |
| API Key + IP Whitelist | Consumer microservices (/get-proxy, /webhook) | Key must match ALLOWED_API_KEY AND request IP must be in IP_ALLOWED | Authorization: <allowed_api_key> from whitelisted IP |
[Admin] POST /api/proxies
-> Proxy created in DB: is_active=true, in_use=0, is_error=false
↓ Cron every 10s
[Cron] GetManyCanRotate(20)
Filter: is_active=true AND is_rotating=false AND (
next_rotate expired AND (is_error=true OR used_count > 0 OR in_use=0)
)
Lock: set is_rotating=true
-> rotateProxy(proxy)
GET rotate_url (optional: Authorization: Bearer auth_token)
Parse new ip/port/username/password from response
-> checkIP(proxy) [up to 3 retries, 5s between each]
Fetch https://api.ipify.org via the proxy
Compare new ip_public to old value
If SUCCESS:
ip_public = new IP
next_rotate = now + rotate_time seconds
in_use = 0, used_count = 0, proxy_used = []
is_error = false, status_code = 200
If FAIL:
is_error = true, response = "Check IP failed", status_code = 500
-> is_rotating = false -> save()
[Consumer] GET /api/get-proxy?area=vn
-> getOneFree(consumerIP, area) [mutex-protected]
Filter: is_active=true, is_rotating=false,
in_use < PROXY_USE_MAX, used_count < PROXY_USE_MAX,
next_rotate > (now - 4min) OR used_count = 0
Sort by: used_count ASC, last_used_at ASC
-> in_use += 1, used_count += 1
-> Append ProxyUsed { ip: consumerIP, key: uuid, time: now }
-> last_used_at = now
-> webhook_url = WEBHOOK_RETURN_PROXY/{id}?key={key}
-> Return { ip, port, username, password, auth_token, webhook_url, ... }
[Consumer] Uses the proxy for its HTTP requests...
[Consumer] POST /webhook/return-proxy/{id}?key={key}
-> ReturnProxy(id, consumerIP, key) [mutex-protected]
Remove matching entry from proxy_used
in_use -= 1
-> save()POST /api/proxies/import).xlsx file via multipart formxlsx libraryhost, port, area, rotate_url, provider, package_code, expired_atproxy_package_id by matching provider name + package_code against DBPOST /api/proxies/import-kiot)API_KIOT_PROXY_PACKAGEAPI_KIOT_PROXY_KEY (using Api-token: KIOT_API_KEY header)KiotProxy provider record if not existsproxy_package if not exists (code: KiotProxy_<code>)expirationAt < nowrotate_url)is_error=true, next_rotate=now (forces immediate rotation on first cron run)POST /api/proxies/import-zing or cron every 12h)API_ZING_PROXY_ALLZingProxy provider + ZingProxy_Pack package if not existtool/
├── src/
│ ├── index.ts # Entry point
│ │ # - Connect MongoDB
│ │ # - Register Swagger, routes, error handler
│ │ # - Start Bun HTTP server
│ │
│ ├── api/
│ │ ├── index.ts # Route groups:
│ │ │ # apiRoutes prefix: /api
│ │ │ # webhookRoutes prefix: /webhook
│ │ │
│ │ ├── auth/
│ │ │ └── auth.controller.ts # POST /api/login
│ │ │ # GET /api/me
│ │ │ # POST /api/register
│ │ │ # No auth required
│ │ │
│ │ ├── user/
│ │ │ ├── user.controller.ts # GET /api/users/users
│ │ │ │ # POST /api/users/users
│ │ │ │ # PUT /api/users/users/:id
│ │ │ │ # DEL /api/users/users/:id
│ │ │ ├── user.service.ts # findAll, findOne, create, update, delete
│ │ │ ├── user.model.ts # Collection: users
│ │ │ └── user.dto.ts # CreateUserSchema {username, email, password}
│ │ │
│ │ ├── proxy/
│ │ │ ├── proxy.controller.ts # GET /api/proxies?get_full=
│ │ │ │ # POST /api/proxies
│ │ │ │ # PUT /api/proxies/:id
│ │ │ │ # POST /api/proxies/bulk-delete
│ │ │ │ # DEL /api/proxies/:id
│ │ │ │ # POST /api/proxies/import
│ │ │ │ # POST /api/proxies/import-kiot
│ │ │ │ # POST /api/proxies/import-zing
│ │ │ │ # POST /api/proxies/drop
│ │ │ │ # Cron: proxy-cron-rotate (10s)
│ │ │ │ # Cron: zing-proxy-cron (12h)
│ │ │ │
│ │ │ ├── proxy.service.ts # Core service (all proxy business logic)
│ │ │ │ # getOneFree(), ReturnProxy() <- mutex
│ │ │ │ # GetManyCanRotate(), UpdateIsRotating()
│ │ │ │ # handleImportKiotProxy(), handleImportZingProxy()
│ │ │ │ # processProxyImport() (Excel)
│ │ │ │
│ │ │ ├── proxy.cron.ts # CronProxy class
│ │ │ │ # start() -> runProcess()
│ │ │ │ # processRotateProxy(): rotate + IP check flow
│ │ │ │ # rotateProxy(): calls rotate_url
│ │ │ │ # runWebhook(): outbound webhook callbacks
│ │ │ │
│ │ │ ├── proxy.model.ts # Collection: proxies
│ │ │ │ # Enum: Status (PENDING/PROCESSING/COMPLETED)
│ │ │ │ # Interface: ProxyUsed, ProxyResponse
│ │ │ │ # toProxyResponse() / toFullProxyResponse()
│ │ │ │
│ │ │ └── proxy.dto.ts # CreateProxySchema
│ │ │ # UpdateProxySchema
│ │ │ # ImportProxySchema
│ │ │
│ │ ├── proxy_package/
│ │ │ ├── proxy_package.controller.ts # GET /api/proxy_packages
│ │ │ │ # POST /api/proxy_packages
│ │ │ │ # PUT /api/proxy_packages/:id
│ │ │ │ # POST /api/proxy_packages/bulk-delete
│ │ │ │ # DEL /api/proxy_packages/:id
│ │ │ │
│ │ │ ├── proxy_package.service.ts # getAll, getById, getByCode, create, update, delete
│ │ │ │ # getKiotPackages() - fetch from KiotProxy API
│ │ │ │ # getKiotKeys() - fetch from KiotProxy API
│ │ │ │ # getOrCreateZingPackage()
│ │ │ │
│ │ │ ├── proxy_package.model.ts # Collection: proxy_packages
│ │ │ │ # Interfaces: KiotProxyKey, KiotProxyPackage,
│ │ │ │ # KiotProxyResponse, KiotProxyKeyResponse
│ │ │ │
│ │ │ └── proxy_package.dto.ts # CreateProxyPackageSchema
│ │ │ # UpdateProxyPackageSchema
│ │ │
│ │ ├── proxy_provider/
│ │ │ ├── proxy_provider.controller.ts # GET /api/proxy_providers
│ │ │ │ # POST /api/proxy_providers
│ │ │ │ # PUT /api/proxy_providers/:id
│ │ │ │ # POST /api/proxy_providers/bulk-delete
│ │ │ │ # DEL /api/proxy_providers/:id
│ │ │ │
│ │ │ ├── proxy_provider.service.ts # findAll, getById, create, update, delete
│ │ │ │ # getOrCreateKiotProvider()
│ │ │ │ # getOrCreateZingProvider()
│ │ │ │
│ │ │ ├── proxy_provider.model.ts # Collection: proxy_providers
│ │ │ └── proxy_provider.dto.ts # CreateProxyProviderSchema, UpdateProxyProviderSchema
│ │ │
│ │ ├── get-proxy/
│ │ │ └── get-proxy.controller.ts # GET /api/get-proxy?area=
│ │ │ # Auth: API Key + IP whitelist
│ │ │ # -> proxyServices.getOneFree(clientIP, area)
│ │ │
│ │ ├── webhook/
│ │ │ └── webhook.controller.ts # POST /webhook/return-proxy/:id?key=
│ │ │ # Auth: API Key + IP whitelist
│ │ │ # -> proxyServices.ReturnProxy(id, ip, key)
│ │ │
│ │ └── test/
│ │ └── test.controller.ts # Simple test/ping endpoint
│ │
│ ├── services/ # Shared infrastructure services
│ │ ├── check-proxy/
│ │ │ ├── checkIP.ts # Verify proxy by fetching api.ipify.org/api.myip.com
│ │ │ │ # Returns resolved public IP or null
│ │ │ └── checkRegion.ts # Detect geographic region of a proxy IP
│ │ │
│ │ └── webhook/
│ │ ├── index.ts # callbackWebhook(): HTTP POST to consumer's URL
│ │ └── configs.ts # Webhook timeout / retry config
│ │
│ ├── config/
│ │ └── database/
│ │ └── mongodb.config.ts # connectDatabase(): Mongoose connection setup
│ │
│ └── utils/
│ ├── index.ts # Re-exports: logger, createElysia, env
│ ├── env.ts # Parse & validate all environment variables
│ ├── createElysia.ts # Three Elysia factory functions:
│ │ # createElysia() -> no auth
│ │ # createElysiaWithAuth() -> JWT Bearer middleware
│ │ # createElysiaWithAllowed() -> API Key + IP whitelist
│ ├── logger.ts # Structured logger (file + console, child scopes)
│ ├── error.middleware.ts # APIError class + global Elysia error handler plugin
│ ├── file/ # File utility helpers
│ └── number/ # Number utility helpers
│
├── .docker/
│ └── dev/
│ └── Dockerfile # Dev Dockerfile (Bun base image)
│
├── docker-compose.yml # Full stack:
│ # proxy_service_tool IP: 11.0.5.10 port: 33033
│ # proxy_service_mongodb IP: 11.0.5.11 port: 33027
│ # proxy_service_redis port: 33025
│ # Network: bridge 11.0.5.0/24
│
├── storage/
│ └── log/ # Runtime log files (gitignored)
│
├── .env.example # Environment variable template
├── package.json # Project manifest & scripts
├── tsconfig.json # TypeScript config + path aliases:
│ # @api -> src/api
│ # @utils -> src/utils
│ # @services -> src/services
│ # @/ -> src/
└── .oxlintrc.json # OXLint linting rule configuration| Pattern | Location | Purpose |
|---|---|---|
| Mutex (async-mutex) | proxy.service.ts — getOneFree(), ReturnProxy(), GetManyCanRotate() | Prevent race conditions when multiple concurrent requests compete for proxy lease slots |
| Elysia Factory Functions | utils/createElysia.ts | Keep Elysia instance setup DRY by injecting the correct auth middleware per security tier |
| Typegoose Decorators | All *.model.ts files | Type-safe Mongoose schemas without duplicating TypeScript interfaces |
| DTO Schema as Single Source | All *.dto.ts files | t.Object() validates at request time; TypeScript type derived via typeof Schema.static — no duplication |
| Service Singleton | End of each *.service.ts | export const proxyServices = new ProxyServices() — one shared instance per process |
| Child Loggers | Each service class | logger.child("proxy") scopes log entries with a service tag for easy filtering |
| Cron co-located with Controller | proxy.controller.ts | Keeps cron lifecycle bound to the route module that owns the domain logic |