StoneForm is a production-ready RESTful API for investment, banking, and user management. Built with Go, MySQL, Redis, and Docker.
- JWT Authentication with refresh tokens
- Rate Limiting and security middleware
- Investment Products with daily returns
- Bank Account Management
- Forum & Task System
- Payment Integration (Kytapay)
- Production-ready Docker deployment
- Security hardened with best practices
- Backend: Go 1.25 with Gin framework
- Database: MySQL 8.0 with connection pooling
- Cache: Redis 7 with persistence
- Deployment: Docker Compose with Nginx reverse proxy
- Security: TLS, rate limiting, input validation
- API Documentation - Complete endpoint reference
- Production Deployment - Step-by-step deployment guide
- Security Recommendations - Security best practices
- Database Hardening - Database security guidelines
# Clone repository
git clone <your-repo-url>
cd stoneform-backend
# Copy environment template
cp env.example .env
# Start development environment
docker compose -f docker-compose.dev.yml up -d
# Run application locally
go run main.go# Setup production environment
cp env.example .env
# Edit .env with production values
# Deploy with Docker Compose
docker compose up -d --build
# Check status
docker compose ps
docker compose logs -fπ For detailed deployment instructions, see how-to-run.md
# Application
ENV=production
PORT=8080
# Database
DB_HOST=db
DB_USER=your_db_user
DB_PASS=your_secure_password
DB_NAME=your_database_name
# Security
JWT_SECRET=your_very_secure_jwt_secret_key_minimum_32_characters
# Redis
REDIS_ADDR=redis:6379
REDIS_PASS=your_redis_passwordπ See env.example for complete configuration options
- Base URL:
https://api.yourdomain.com/api(production) orhttp://localhost:8080/api(development) - Authentication: JWT Bearer tokens
- Rate Limiting: Implemented per endpoint
- Response Format: JSON with
success,message,datafields
PORT=8080
JWT_SECRET=supersecretjwtkey
WITHDRAWAL_CHARGE_PERCENT=10.0
CRON_KEY=supersecretcronkey
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASS=
DB_NAME=sf
KYTAPAY_CLIENT_ID=
KYTAPAY_CLIENT_SECRET=
NOTIFY_URL=https://yourdomain.com/api/callback/payment
SUCCESS_URL=https://yourdomain.com/payment/success
FAILED_URL=https://yourdomain.com/payment/failed
# Optional: full DSN (overrides DB_HOST/PORT/USER/PASS/NAME if set)
# Example for Docker: root:123456789@tcp(db:3306)/v1?charset=utf8mb4&parseTime=True&loc=Local
DB_DSN=
| Method | Endpoint | Brief Description |
|---|---|---|
| POST | /register | Register new user and returns JWT token |
| POST | /login | Login, returns JWT token |
| GET | /ping | Protected ping (JWT required) |
| GET | /users/info | Get user info (JWT required) |
| POST | /users/change-password | Change password (JWT required) |
| GET | /products | List investment products |
| POST | /users/investments | Create investment (JWT required) |
| GET | /users/investments | List user investments (JWT required) |
| GET | /users/investments/{id} | Get investment detail (JWT required) |
| POST | /users/withdrawal | Withdraw funds (JWT required) |
| GET | /users/bank | List user bank accounts (JWT required) |
| POST | /users/bank | Add bank account (JWT required) |
| PUT | /users/bank | Edit bank account (JWT required) |
| DELETE | /users/bank | Delete bank account (JWT required) |
| GET | /bank | List supported banks (JWT required) |
| GET | /users/task | List user tasks (JWT required) |
| POST | /users/task/submit | Submit task (JWT required) |
| GET | /users/forum | List forum posts (JWT required) |
| POST | /users/forum/submit | Submit forum post (JWT required) |
| POST | /payments/kyta/webhook | Payment webhook (no auth) |
| POST | /cron/daily-returns | Cron: process daily returns (X-CRON-KEY) |
POST /api/register
- Registers a new user.
- Headers:
Content-Type: application/json - Request Body:
{
"name": "John Doe",
"number": "0812000000011",
"password": "secret12",
"password_confirmation": "secret12",
"referral_code": "" // optional
}- Success Response:
{
"success": true,
"message": "Registration successful",
"data": {
"token": "<jwt-token>",
"expired_at": "2025-08-28T01:23:45Z",
"data": {
"id": 1,
"name": "John Doe",
"number": "0812000000011",
...
}
}
}- Error Response:
{
"success": false,
"message": "number already registered"
}POST /api/login
- Authenticates user and returns JWT token.
- Headers:
Content-Type: application/json - Request Body:
{
"number": "0812000000011",
"password": "secret12"
}- Success Response:
{
"success": true,
"message": "Login successful",
"data": {
"token": "<jwt-token>",
"expired_at": "2025-08-28T01:23:45Z",
"data": {
"id": 1,
"name": "John Doe",
"number": "0812000000011",
...
}
}
}- Error Response:
{
"success": false,
"message": "Invalid number or password"
}GET /api/users/info
- Returns user information.
- Headers:
Authorization: Bearer <token> - Success Response:
{
"success": true,
"data": {
"id": 1,
"name": "John Doe",
"number": "0812000000011",
...
}
}GET /api/products
- Lists available investment products.
- Success Response:
{
"success": true,
"data": {
"products": [
{ "id": 1, "name": "Star 1", "min": 100000, "max": 1000000 },
{ "id": 2, "name": "Star 2", "min": 1000000, "max": 10000000 },
...
]
}
}POST /api/users/investments
- Create a new investment for the user.
- Headers:
Authorization: Bearer <token>,Content-Type: application/json - Request Body:
{
"product_id": 3,
"amount": 5000000,
"payment_method": "BANK",
"payment_channel": "BCA"
}- Success Response:
{
"success": true,
"data": {
"investment_id": 123,
"payment": {
"account_number": "1234567890",
"expiry": "2025-09-10T12:00:00Z"
}
}
}POST /api/users/withdrawal
- Withdraw funds to a bank account.
- Headers:
Authorization: Bearer <token>,Content-Type: application/json - Request Body:
{
"amount": 99000,
"bank_account_id": 1
}- Success Response:
{
"success": true,
"message": "Withdrawal request submitted"
}POST /api/users/bank
- Add a new bank account for the user.
- Headers:
Authorization: Bearer <token>,Content-Type: application/json - Request Body:
{
"bank_id": 2,
"account_name": "John Doe",
"account_number": "757654123611"
}- Success Response:
{
"success": true,
"message": "Bank account added"
}DELETE /api/users/bank
- Delete a user's bank account.
- Headers:
Authorization: Bearer <token>,Content-Type: application/json - Request Body:
{
"id": 1
}- Success Response:
{
"success": true,
"message": "Bank account deleted"
}- Error Response:
{
"success": false,
"message": "Unauthorized"
}POST /api/payments/kyta/webhook
- Payment provider callback. No authentication required.
- Request Body:
{
"reference_id": "PUT_ORDER_ID_HERE",
"status": "PAID",
"response_code": "2000500",
"id": "payment-123"
}- Success Response:
{
"success": true
}POST /api/cron/daily-returns
- Internal cron endpoint. Requires
X-CRON-KEYheader. - Headers:
X-CRON-KEY: <your_cron_key> - Success Response:
{
"success": true,
"message": "Processed daily returns"
}- Register/Login: 10 requests/minute/IP
- User endpoints (read): 120 requests/minute/user (e.g., GET /users/info, /users/investments)
- User endpoints (write): 60 requests/minute/user (e.g., POST/PUT/DELETE)
- Withdraw/Transfer: 20 requests/minute/user (if implemented separately)
- Webhook: 500 requests/hour/IP (no auth, sliding window, whitelisted IPs unlimited)
- Cron: 1000 requests/hour/IP
- Register
- Login (get JWT token)
- Get user info (use token)
- List products
- Create investment
- Add bank account
- Withdraw
# Register
curl -X POST https://api.domain.com/api/register \
-H "Content-Type: application/json" \
-d '{"name":"John","number":"081200000001","password":"secret12","password_confirmation":"secret12"}'
# Login
curl -X POST https://api.domain.com/api/login \
-H "Content-Type: application/json" \
-d '{"number":"081200000001","password":"secret12"}'
# Use token for next requests
curl https://api.domain.com/api/users/info \
-H "Authorization: Bearer <jwt-token>"
# List products
curl https://api.domain.com/api/products
# Create investment
curl -X POST https://api.domain.com/api/users/investments \
-H "Authorization: Bearer <jwt-token>" \
-H "Content-Type: application/json" \
-d '{"product_id":3,"amount":5000000,"payment_method":"BANK","payment_channel":"BCA"}'
# Add bank account
curl -X POST https://api.domain.com/api/users/bank \
-H "Authorization: Bearer <jwt-token>" \
-H "Content-Type: application/json" \
-d '{"bank_id":2,"account_name":"John Doe","account_number":"757654123611"}'
# Withdraw
curl -X POST https://api.domain.com/api/users/withdrawal \
-H "Authorization: Bearer <jwt-token>" \
-H "Content-Type: application/json" \
-d '{"amount":99000,"bank_account_id":1}'Common error codes and responses:
| Code | Meaning | Example Response |
|---|---|---|
| 400 | Bad Request | { "success": false, "message": "Invalid request" } |
| 401 | Unauthorized | { "success": false, "message": "Unauthorized" } |
| 403 | Forbidden | { "success": false, "message": "Forbidden" } |
| 429 | Too Many Requests | { "success": false, "message": "Too many requests. Please try again later." } |
| 500 | Server Error | { "success": false, "message": "Internal server error" } |
All error responses are JSON with success: false and a message field.
- Copy
.env.exampleto.env, ensure DB settings match docker-compose. - Start services:
docker compose up -d --build
- Verify:
docker compose ps docker compose logs -f app
- Test endpoints:
- Register:
curl -X POST http://localhost:8080/api/register \ -H "Content-Type: application/json" \ -d '{"name":"John","number":"081200000001","password":"secret12","password_confirmation":"secret12"}'
- Login:
Successful response:
curl -X POST http://localhost:8080/api/login \ -H "Content-Type: application/json" \ -d '{"number":"081200000001","password":"secret12"}'
{ "success": true, "message": "Login successful", "data": { "token": "<jwt-token>", "expired_at": "2025-08-28T01:23:45Z" } } - Use token on next request (protected example):
curl http://localhost:8080/api/ping \ -H "Authorization: Bearer <jwt-token>"
- Register:
- Token contains claims: id, name, exp.
- Expiration is 6 hours from login; after that, token is invalid.
- Middleware reads Authorization: Bearer , verifies with JWT_SECRET.
- Expired token returns:
{ "success": false, "message": "Token expired, please login again" }
- The server supports immediate access-token revocation by storing the token's
jtiin a revocation store. - If
REDIS_ADDRis configured the server will use Redis as the revocation store and check the keyjwt:blacklist:<jti>on each request. If that key is present the access token is rejected. - If Redis is not configured, the server will fall back to checking a
revoked_tokensDB table. - Use the helper
utils.RevokeJTI(jti, ttl)in logout or revoke endpoints. If Redis is used this sets a key with the provided TTL; otherwise it inserts intorevoked_tokens.
Example (server-side):
- Parse access token and extract
jtiandexp. - Compute
ttl := time.Until(exp). - Call
utils.RevokeJTI(jti, ttl)to ensure the token is rejected until expiry.
- Set
JWT_SECRETin your.env(required for token signing/verification). - Database config via
.env: DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME (or DB_DSN).
This update replaces the old deposit top-up flow with direct Investments using fixed Products (Star 1, Star 2, Star 3). Payments still use Kytapay (QRIS / Virtual Account). Daily returns are processed via a cron endpoint.
Run the SQL files in the migrations folder in order (ensure you already created users, transactions, banks, etc.):
- create_products_table.sql
- seed_products.sql
- create_investments_table.sql
- 20250907_alter_investments_add_payment_fields.sql
These create and seed:
- products: Star 1/2/3 with min/max, percentage, duration
- investments: tracks user investments, daily profit, status, schedule
Add the following to your .env:
- KYTAPAY_BASE_URL (default: https://api.kytapay.com/v2)
- KYTAPAY_CLIENT_ID
- KYTAPAY_CLIENT_SECRET
- NOTIFY_URL (Kytapay webhook URL -> e.g. https://yourdomain/api/payments/kyta/webhook)
- SUCCESS_URL (redirect after successful payment)
- FAILED_URL (redirect after failed payment)
- CRON_KEY (secret used by the cron endpoint)
-
GET /api/products
- Public. Lists active products (Star 1/2/3).
-
POST /api/users/investments (protected)
- Body: { product_id, amount, payment_method: "QRIS"|"BANK", payment_channel: "BCA"|"BRI"|"BNI"|"MANDIRI"|"PERMATA"|"BNC" (if BANK) }
- Validates amount within product min/max, creates Kytapay payment, creates Investment (Pending) + a Transaction (Pending), and returns payment details (qr_string or account_number) and expiry.
-
GET /api/users/investments (protected)
- List user investments.
-
GET /api/users/investments/{id} (protected)
- Get single investment detail.
-
POST /api/payments/kyta/webhook
- Kytapay callback (no auth). On success, mark investment Running, set next_return_at to +24h, mark the related transaction Success, and increment user.total_invest.
-
POST /api/cron/daily-returns
- Cron endpoint protected via header: X-CRON-KEY: <CRON_KEY>
- Processes due investments (status Running, next_return_at <= now). Credits daily profit to user balance, adds a Success transaction of type investment_profit, updates schedule and marks Completed when total_paid == duration.
- The old deposit route is removed from the router. Payment utilities from deposit code are reused internally for investments.
- Transaction types used: "investment" for the initial top-up and "investment_profit" for daily returns.
- Daily profit formula: amount * (percentage/100) / duration, rounded to 2 decimals.
- Payment gateway details are stored with each investment for later access: payment_method, payment_channel, payment_code (qr_string or VA number), payment_link, expired_at.
Use test.http for example requests:
- Register/Login and capture token.
- GET /api/products
- POST /api/users/investments (QRIS or BANK)
- Simulate webhook: POST /api/payments/kyta/webhook
- Trigger cron: POST /api/cron/daily-returns with X-CRON-KEY header.
- VPS with Ubuntu 20.04+ or Debian 11+
- Docker & Docker Compose installed
- Domain name (optional, for HTTPS)
# 1. Clone repository
git clone <your-repo-url>
cd stoneform-backend
# 2. Setup environment
cp env.example .env
nano .env # Edit with production values
# 3. Deploy
docker compose up -d --build
# 4. Check status
docker compose ps
curl http://localhost:8080/health# Deploy with Nginx
docker compose --profile nginx up -d --build
# Setup SSL (optional)
sudo certbot certonly --standalone -d yourdomain.com# View logs
docker compose logs -f
# Backup database
docker compose exec db mysqldump -u root -p$DB_ROOT_PASSWORD $DB_NAME > backup.sql
# Update application
git pull && docker compose up -d --buildπ Complete deployment guide: how-to-run.md
This application implements production-grade security features:
- Authentication: JWT with refresh tokens
- Authorization: Role-based access control
- Rate Limiting: Per-endpoint and per-user limits
- Input Validation: Comprehensive request validation
- Security Headers: CORS, CSRF protection, HSTS
- Database Security: TLS connections, connection pooling
- Container Security: Non-root user, minimal base image
π Security best practices: SECURITY-RECOMMENDATIONS.md
# Start development environment
docker compose -f docker-compose.dev.yml up -d
# Run application locally
go run main.go
# Run tests
go test ./...
# Format code
go fmt ./...βββ controllers/ # HTTP handlers
βββ models/ # Database models
βββ middleware/ # HTTP middleware
βββ routes/ # Route definitions
βββ database/ # Database connection
βββ utils/ # Utility functions
βββ migrations/ # Database migrations
βββ scripts/ # Development scripts
- Documentation: See individual
.mdfiles - Issues: Create GitHub issue
- Security: Report to security@yourcompany.com
[Your License Here]
π StoneForm Backend - Production Ready Go API