# Sub2ApiPay **Language**: [中文](./README.md) | English (current) Sub2ApiPay is a self-hosted payment gateway built for the [Sub2API](https://sub2api.com) platform. It supports four payment channels — **EasyPay** (aggregated Alipay/WeChat Pay), **Alipay** (official), **WeChat Pay** (official), and **Stripe** — with both pay-as-you-go balance top-up and subscription plans. Once a payment is confirmed, the system automatically calls the Sub2API management API to credit the user's balance or activate the subscription — no manual intervention required. --- ## Table of Contents - [Features](#features) - [Tech Stack](#tech-stack) - [Quick Start](#quick-start) - [Environment Variables](#environment-variables) - [Deployment](#deployment) - [Sub2API Integration](#sub2api-integration) - [Payment Flow](#payment-flow) - [API Endpoints](#api-endpoints) - [Development](#development) --- ## Features - **Four Payment Channels** — EasyPay aggregation, Alipay (official), WeChat Pay (official), Stripe - **Dual Billing Modes** — Pay-as-you-go balance top-up + subscription plans - **Auto Balance Credit** — Automatically calls Sub2API after payment verification, fully hands-free - **Full Order Lifecycle** — Auto-expiry, user cancellation, admin cancellation, refunds - **Limit Controls** — Per-transaction cap, daily per-user cap, daily per-channel global cap - **Security** — Token auth, RSA2/MD5/Webhook signature verification, timing-safe comparison, full audit log - **Responsive UI** — PC + mobile adaptive layout, dark/light theme, iframe embed support - **Bilingual** — Automatic Chinese/English interface adaptation - **Admin Panel** — Dashboard, order management (pagination/filtering/retry/refund), channel & subscription management > **EasyPay Recommendation**: We personally recommend [ZPay](https://z-pay.cn/?uid=23808) as an EasyPay provider (referral link — feel free to remove). ZPay supports **individual users** (no business license) with a daily limit of ¥10,000; business license holders have no limit. Please evaluate the security, reliability, and compliance of any third-party payment provider on your own — this project does not endorse or guarantee any specific provider.
ZPay Registration QR Code ![ZPay Preview](./docs/zpay-preview.png)
--- ## Tech Stack | Category | Technology | | --------------- | -------------------------- | | Framework | Next.js 16 (App Router) | | Language | TypeScript 5 + React 19 | | Styling | TailwindCSS 4 | | ORM | Prisma 7 (adapter-pg mode) | | Database | PostgreSQL 16 | | Container | Docker + Docker Compose | | Package Manager | pnpm | --- ## Quick Start ### Using Docker Hub Image (Recommended) No Node.js or pnpm required on the server — just Docker. ```bash mkdir -p /opt/sub2apipay && cd /opt/sub2apipay # Download Compose file and env template curl -O https://raw.githubusercontent.com/touwaeriol/sub2apipay/main/docker-compose.hub.yml curl -O https://raw.githubusercontent.com/touwaeriol/sub2apipay/main/.env.example cp .env.example .env # Fill in required environment variables nano .env # Start (includes bundled PostgreSQL) docker compose -f docker-compose.hub.yml up -d ``` ### Build from Source ```bash git clone https://github.com/touwaeriol/sub2apipay.git cd sub2apipay cp .env.example .env nano .env docker compose up -d --build ``` --- ## Environment Variables See [`.env.example`](./.env.example) for the full template. ### Core (Required) | Variable | Description | | ----------------------- | ---------------------------------------------------------- | | `SUB2API_BASE_URL` | Sub2API service URL, e.g. `https://sub2api.com` | | `SUB2API_ADMIN_API_KEY` | Sub2API admin API key | | `ADMIN_TOKEN` | Admin panel access token (use a strong random string) | | `NEXT_PUBLIC_APP_URL` | Public URL of this service, e.g. `https://pay.example.com` | > `DATABASE_URL` is automatically injected by Docker Compose when using the bundled database. ### Payment Providers & Methods **Step 1**: Declare which payment providers to load via `PAYMENT_PROVIDERS` (comma-separated): ```env # Available: easypay, alipay, wxpay, stripe # Example: EasyPay only PAYMENT_PROVIDERS=easypay # Example: Alipay + WeChat Pay + Stripe (official channels) PAYMENT_PROVIDERS=alipay,wxpay,stripe ``` > **Alipay / WeChat Pay (official)** and **EasyPay** can coexist. Official channels connect directly to Alipay/WeChat Pay APIs with funds going straight to your merchant account and lower fees; EasyPay proxies payments through a third-party platform that forwards to official channels, with a lower barrier to entry. When using EasyPay, choose providers where funds are forwarded through official channels directly to your own account, rather than collected by a third party. #### EasyPay (Alipay / WeChat Pay Aggregation) Any payment provider compatible with the **EasyPay protocol** can be used. | Variable | Description | | --------------------- | ---------------------------------------------------------------- | | `EASY_PAY_PID` | EasyPay merchant ID | | `EASY_PAY_PKEY` | EasyPay merchant secret key | | `EASY_PAY_API_BASE` | EasyPay API base URL | | `EASY_PAY_NOTIFY_URL` | Async callback URL: `${NEXT_PUBLIC_APP_URL}/api/easy-pay/notify` | | `EASY_PAY_RETURN_URL` | Redirect URL after payment: `${NEXT_PUBLIC_APP_URL}/pay/result` | | `EASY_PAY_CID_ALIPAY` | Alipay channel ID (optional) | | `EASY_PAY_CID_WXPAY` | WeChat Pay channel ID (optional) | #### Alipay (Official) Direct integration with the Alipay Open Platform. Supports PC page payment (`alipay.trade.page.pay`) and mobile web payment (`alipay.trade.wap.pay`), automatically switching based on device type. | Variable | Description | | -------------------- | ---------------------------------------------- | | `ALIPAY_APP_ID` | Alipay application AppID | | `ALIPAY_PRIVATE_KEY` | Application private key (content or file path) | | `ALIPAY_PUBLIC_KEY` | Alipay public key (content or file path) | | `ALIPAY_NOTIFY_URL` | Async callback URL | | `ALIPAY_RETURN_URL` | Sync redirect URL (optional) | #### WeChat Pay (Official) Direct integration with WeChat Pay APIv3. Supports Native QR code payment and H5 payment, with mobile devices preferring H5 and auto-fallback to QR code. | Variable | Description | | --------------------- | ----------------------------------------------- | | `WXPAY_APP_ID` | WeChat Pay AppID | | `WXPAY_MCH_ID` | Merchant ID | | `WXPAY_PRIVATE_KEY` | Merchant API private key (content or file path) | | `WXPAY_CERT_SERIAL` | Merchant certificate serial number | | `WXPAY_API_V3_KEY` | APIv3 key | | `WXPAY_PUBLIC_KEY` | WeChat Pay public key (content or file path) | | `WXPAY_PUBLIC_KEY_ID` | WeChat Pay public key ID | | `WXPAY_NOTIFY_URL` | Async callback URL | #### Stripe | Variable | Description | | ------------------------ | ------------------------------------------- | | `STRIPE_SECRET_KEY` | Stripe secret key (`sk_live_...`) | | `STRIPE_PUBLISHABLE_KEY` | Stripe publishable key (`pk_live_...`) | | `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret (`whsec_...`) | > Stripe webhook endpoint: `${NEXT_PUBLIC_APP_URL}/api/stripe/webhook` > Subscribe to: `payment_intent.succeeded`, `payment_intent.payment_failed` ### Business Rules | Variable | Description | Default | | -------------------------------- | ------------------------------------------------- | -------------------------- | | `MIN_RECHARGE_AMOUNT` | Minimum amount per transaction (CNY) | `1` | | `MAX_RECHARGE_AMOUNT` | Maximum amount per transaction (CNY) | `1000` | | `MAX_DAILY_RECHARGE_AMOUNT` | Daily cumulative max per user (`0` = unlimited) | `10000` | | `MAX_DAILY_AMOUNT_ALIPAY` | EasyPay Alipay channel daily global limit (opt.) | Provider default | | `MAX_DAILY_AMOUNT_ALIPAY_DIRECT` | Alipay official channel daily global limit (opt.) | Provider default | | `MAX_DAILY_AMOUNT_WXPAY` | WeChat Pay channel daily global limit (opt.) | Provider default | | `MAX_DAILY_AMOUNT_STRIPE` | Stripe channel daily global limit (opt.) | Provider default | | `ORDER_TIMEOUT_MINUTES` | Order expiry in minutes | `5` | | `PRODUCT_NAME` | Product name shown on payment page | `Sub2API Balance Recharge` | ### UI Customization (Optional) Display a support contact image and description on the right side of the payment page. | Variable | Description | | -------------------- | ------------------------------------------------------------------------------- | | `PAY_HELP_IMAGE_URL` | Help image URL — external URL or local path (see below) | | `PAY_HELP_TEXT` | Help text; use `\n` for line breaks, e.g. `Scan to add WeChat\nMon–Fri 9am–6pm` | **Two ways to provide the image:** - **External URL** (recommended — no Compose changes needed): any publicly accessible image link (CDN, OSS, image hosting). ```env PAY_HELP_IMAGE_URL=https://cdn.example.com/help-qr.jpg ``` - **Local file**: place the image in `./uploads/` and reference it as `/uploads/`. The directory must be mounted in `docker-compose.app.yml` (included by default): ```yaml volumes: - ./uploads:/app/public/uploads:ro ``` ```env PAY_HELP_IMAGE_URL=/uploads/help-qr.jpg ``` > Clicking the help image opens it full-screen in the center of the screen. ### Docker Compose Variables | Variable | Description | Default | | ------------- | -------------------------------- | ------------------------------------- | | `APP_PORT` | Host port mapping | `3001` | | `DB_PASSWORD` | PostgreSQL password (bundled DB) | `password` (**change in production**) | --- ## Deployment ### Option 1: Docker Hub Image + Bundled Database Use `docker-compose.hub.yml` — the simplest deployment: ```bash docker compose -f docker-compose.hub.yml up -d ``` Image: [`touwaeriol/sub2apipay:latest`](https://hub.docker.com/r/touwaeriol/sub2apipay) ### Option 2: Docker Hub Image + External Database For existing PostgreSQL instances (shared with other services): 1. Set `DATABASE_URL` in `.env` 2. Use `docker-compose.app.yml` (app only, no DB): ```bash docker compose -f docker-compose.app.yml up -d ``` ### Option 3: Build from Source For custom builds after modifications: ```bash # On the build server docker compose build docker tag sub2apipay-app:latest touwaeriol/sub2apipay:latest docker push touwaeriol/sub2apipay:latest # On the deploy server docker compose -f docker-compose.hub.yml pull docker compose -f docker-compose.hub.yml up -d ``` ### Reverse Proxy The default host port is `3001` (configurable via `APP_PORT`). Use Nginx or Caddy as a reverse proxy with HTTPS: ```nginx server { listen 443 ssl; server_name pay.example.com; location / { proxy_pass http://127.0.0.1:3001; 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; } } ``` ### Database Migrations Migrations run automatically on container startup via `prisma migrate deploy`. To run manually: ```bash docker compose exec app npx prisma migrate deploy ``` --- ## Sub2API Integration Assuming this service is deployed at `https://pay.example.com`. ### User-Facing Pages Configure the following URLs in the Sub2API admin panel under **Recharge Settings**, so users can navigate from Sub2API to the payment and order pages: | Setting | URL | Description | | --------- | ------------------------------------ | -------------------------------------------------- | | Payment | `https://pay.example.com/pay` | User top-up & subscription purchase page | | My Orders | `https://pay.example.com/pay/orders` | User views their own recharge/subscription history | Sub2API **v0.1.88** and above will automatically append the following parameters — no manual query string needed: | Parameter | Description | | --------- | ------------------------------------------------- | | `user_id` | Sub2API user ID | | `token` | User login token (required to view order history) | | `theme` | `light` (default) or `dark` | | `lang` | Interface language, `zh` (default) or `en` | | `ui_mode` | `standalone` (default) or `embedded` (for iframe) | ### Admin Panel The admin panel is authenticated via the `token` URL parameter (set to the `ADMIN_TOKEN` environment variable). When integrating with Sub2API, just configure the paths — **no query parameters needed** — Sub2API will automatically append `token` and other parameters: | Page | URL | Description | | ------------- | --------------------------------------------- | ------------------------------------------------------------- | | Overview | `https://pay.example.com/admin` | Aggregated entry with card-style navigation | | Orders | `https://pay.example.com/admin/orders` | Filter by status, paginate, view details, retry/cancel/refund | | Dashboard | `https://pay.example.com/admin/dashboard` | Revenue stats, order trends, payment method breakdown | | Channels | `https://pay.example.com/admin/channels` | Configure API channels & rates, sync from Sub2API | | Subscriptions | `https://pay.example.com/admin/subscriptions` | Manage subscription plans & user subscriptions | > **Tip**: When accessing directly (not via Sub2API), you need to manually append `?token=YOUR_ADMIN_TOKEN` to the URL. All admin pages share the same token — once you enter any page, you can navigate between modules via the sidebar. --- ## Payment Flow ``` User selects top-up / subscription plan │ ▼ Create Order (PENDING) ├─ Validate user status / pending orders / daily limit / channel limit └─ Call payment provider to get payment link │ ▼ User completes payment ├─ EasyPay → QR code / H5 redirect ├─ Alipay (official) → PC page payment / H5 mobile web payment ├─ WeChat Pay (official) → Native QR code / H5 payment └─ Stripe → Payment Element (PaymentIntent) │ ▼ Payment callback (RSA2 / MD5 / Webhook signature verified) → Order PAID │ ▼ Auto-call Sub2API recharge / subscription API ├─ Success → COMPLETED, balance credited / subscription activated └─ Failure → FAILED (admin can retry) ``` --- ## API Endpoints All API paths are prefixed with `/api`. ### Public API User-facing endpoints, authenticated via `user_id` + `token` URL parameters. | Method | Path | Description | | ------ | ------------------------- | --------------------------------------------------- | | `GET` | `/api/user` | Get current user info | | `GET` | `/api/users/:id` | Get specific user info | | `POST` | `/api/orders` | Create recharge / subscription order | | `GET` | `/api/orders/:id` | Query order details | | `POST` | `/api/orders/:id/cancel` | User cancels pending order | | `GET` | `/api/orders/my` | List current user's orders | | `GET` | `/api/channels` | Get channel list (for frontend display) | | `GET` | `/api/subscription-plans` | Get available subscription plans | | `GET` | `/api/subscriptions/my` | Query current user's subscriptions | | `GET` | `/api/limits` | Query recharge limits & payment method availability | ### Payment Callbacks Called asynchronously by payment providers; signature verified before triggering credit flow. | Method | Path | Description | | ------ | ---------------------- | ------------------------------ | | `GET` | `/api/easy-pay/notify` | EasyPay async callback | | `POST` | `/api/alipay/notify` | Alipay (official) callback | | `POST` | `/api/wxpay/notify` | WeChat Pay (official) callback | | `POST` | `/api/stripe/webhook` | Stripe webhook callback | ### Admin API Authenticated via `token` parameter set to `ADMIN_TOKEN`. | Method | Path | Description | | -------- | ----------------------------------- | ------------------------------------ | | `GET` | `/api/admin/orders` | Order list (paginated, filterable) | | `GET` | `/api/admin/orders/:id` | Order details (with audit log) | | `POST` | `/api/admin/orders/:id/cancel` | Admin cancels order | | `POST` | `/api/admin/orders/:id/retry` | Retry failed recharge / subscription | | `POST` | `/api/admin/refund` | Issue refund | | `GET` | `/api/admin/dashboard` | Dashboard (revenue stats, trends) | | `GET` | `/api/admin/channels` | Channel list | | `POST` | `/api/admin/channels` | Create channel | | `PUT` | `/api/admin/channels/:id` | Update channel | | `DELETE` | `/api/admin/channels/:id` | Delete channel | | `GET` | `/api/admin/subscription-plans` | Subscription plan list | | `POST` | `/api/admin/subscription-plans` | Create subscription plan | | `PUT` | `/api/admin/subscription-plans/:id` | Update subscription plan | | `DELETE` | `/api/admin/subscription-plans/:id` | Delete subscription plan | | `GET` | `/api/admin/subscriptions` | User subscription records | | `GET` | `/api/admin/config` | Get system configuration | | `PUT` | `/api/admin/config` | Update system configuration | | `GET` | `/api/admin/sub2api/groups` | Sync channel groups from Sub2API | | `GET` | `/api/admin/sub2api/search-users` | Search Sub2API users | --- ## Development ### Requirements - Node.js 22+ - pnpm - PostgreSQL 16+ ### Local Setup ```bash pnpm install cp .env.example .env # Edit .env with DATABASE_URL and other required values pnpm prisma migrate dev pnpm dev ``` ### Commands ```bash pnpm dev # Dev server with hot reload pnpm build # Production build pnpm test # Run tests pnpm typecheck # TypeScript type check pnpm lint # ESLint pnpm format # Prettier format pnpm prisma generate # Generate Prisma client pnpm prisma migrate dev # Create and apply migration (dev) pnpm prisma migrate deploy # Apply migrations (production) pnpm prisma studio # Visual database browser ``` --- ## License MIT