A digital wallet API built in Go, inspired by Telda. Supports user authentication,
Stripe-powered top-ups, user-to-user transfers, transaction history, and scheduled
transfers. EGP only (stored in piastres).
Layer
Choice
Language
Go
Router
Chi
Database
PostgreSQL
Cache
Redis
Migrations
Goose
Query Generation
sqlc
Payments
Stripe
Docs
Swaggo
go-chi/chi — router
go-chi/httprate — rate limiting
golang-jwt/jwt — auth
sqlc-dev/sqlc — query generation
pressly/goose — migrations
swaggo/swag — swagger
redis/go-redis — Redis client
joho/godotenv — config
go-playground/validator — request validation
stripe/stripe-go — Stripe SDK
Nexus/
├── cmd/ # Application entrypoints
│ ├── main.go # Main entrypoint
│ └── app.go # App initialization & routing
├── internal/ # Private application code
│ ├── db/
│ │ ├── postgresql/ # PostgreSQL related
│ │ │ ├── migrations/ # DB migrations (Goose)
│ │ │ └── sqlc/ # Generated SQL queries
│ │ │ └── queries/ # SQL query files
│ │ └── redisDb/ # Redis caching layer
│ ├── security/ # Auth, JWT, password hashing
│ ├── users/ # User handlers, services, types
│ └── utils/ # Utilities
│ ├── api/ # API helpers
│ └── env/ # Environment config
├── docs/ # Swagger documentation
├── compose.yaml # Docker Compose config
├── Dockerfile # Container image
├── go.mod / go.sum # Dependencies
├── sqlc.yaml # sqlc config
├── justfile # Task runner
└── .air.toml # Hot reload config
CREATE TYPE transaction_type AS ENUM (' debit' , ' credit' );
CREATE TYPE transaction_status AS ENUM (' pending' , ' processing' , ' completed' , ' failed' , ' reversed' );
CREATE TYPE transfer_status AS ENUM (' pending' , ' completed' , ' failed' );
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL ,
full_name TEXT NOT NULL ,
refresh_token TEXT ,
token_expires_at TIMESTAMPTZ ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE wallets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL UNIQUE REFERENCES users(id),
balance BIGINT NOT NULL DEFAULT 0 CHECK (balance >= 0 ),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_id UUID NOT NULL REFERENCES wallets(id),
amount BIGINT NOT NULL CHECK (amount > 0 ),
type transaction_type NOT NULL ,
status transaction_status NOT NULL DEFAULT ' pending' ,
transfer_id UUID REFERENCES transfers(id) DEFERRABLE INITIALLY DEFERRED,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_wallet_id UUID NOT NULL REFERENCES wallets(id),
to_wallet_id UUID NOT NULL REFERENCES wallets(id),
amount BIGINT NOT NULL CHECK (amount > 0 ),
status transfer_status NOT NULL DEFAULT ' pending' ,
note TEXT ,
debit_transaction_id UUID UNIQUE REFERENCES transactions(id) DEFERRABLE INITIALLY DEFERRED,
credit_transaction_id UUID UNIQUE REFERENCES transactions(id) DEFERRABLE INITIALLY DEFERRED,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE scheduled_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
transfer_id UUID NOT NULL UNIQUE REFERENCES transfers(id),
scheduled_at TIMESTAMPTZ NOT NULL ,
executed_at TIMESTAMPTZ ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION set_updated_at ()
RETURNS TRIGGER AS $$
BEGIN
NEW .updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_set_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TRIGGER wallets_set_updated_at
BEFORE UPDATE ON wallets
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TRIGGER transfers_set_updated_at
BEFORE UPDATE ON transfers
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TRIGGER scheduled_transfers_set_updated_at
BEFORE UPDATE ON scheduled_transfers
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
Method
Endpoint
Auth
Description
POST
/auth/register
❌
Register a new user
POST
/auth/login
❌
Login and get JWT
POST
/auth/refresh
❌
Refresh JWT token
PATCH
/auth/profile
✅
Update name, email, password
Method
Endpoint
Auth
Description
GET
/wallet
✅
Get wallet + balance
POST
/wallet/topup
✅
Initiate Stripe top-up
POST
/webhook/stripe
❌
Stripe webhook handler
Method
Endpoint
Auth
Description
POST
/transfers
✅
Send money to another user
GET
/transfers
✅
Get sent and received history
GET
/transfers/:id
✅
Get single transfer
Method
Endpoint
Auth
Description
POST
/scheduled
✅
Create a scheduled transfer
GET
/scheduled
✅
List scheduled transfers
GET
/scheduled/:id
✅
Get single scheduled transfer
PATCH
/scheduled/:id
✅
Update a pending scheduled transfer
DELETE
/scheduled/:id
✅
Cancel a scheduled transfer
Method
Endpoint
Auth
Description
GET
/users/search?q=
✅
Search users by name or email
Key
Description
Invalidated On
wallet:balance:{wallet_id}
User's current balance
Every transaction
user:{user_id}
User profile
Profile update
Endpoint
Limit
Per
/transfers
10 req/min
User
/wallet/topup
5 req/min
User
/auth/login
5 req/min
IP
Everything else
60 req/min
User
debit — money sent out
credit — money received or topped up
pending — initiated
processing — being handled
completed — successful
failed — something went wrong
reversed — rolled back
pending — initiated
completed — successful
failed — something went wrong
Scheduled Transfer Statuses
pending — waiting for execution
processing — cron picked it up
completed — executed successfully
failed — cron tried but failed
cancelled — user cancelled
Client calls POST /wallet/topup
Server creates a Stripe Payment Intent
Transaction created with status pending
Stripe processes payment
Stripe fires webhook to POST /webhook/stripe
Server updates transaction to completed or failed
On completed, wallet balance is incremented
Client calls POST /transfers
Server creates a transfers row with status pending, nullable transaction IDs
Server inserts debit transaction (pending) and credit transaction (pending)
Server updates the transfer with both transaction IDs
Execute balance changes within a DB transaction:
Debit sender's wallet
Credit receiver's wallet
Mark both transactions as completed
Mark transfer as completed
On any failure, mark transactions and transfer as failed
Cron Job — Scheduled Transfers
Runs every minute:
Query scheduled_transfers where status = pending AND scheduled_at <= NOW()
Set status to processing
Execute transfer logic (same as regular transfer)
Set status to completed or failed
Note: processing status acts as a crash recovery guard.
Stuck processing records can be detected and retried or flagged.
All money is stored in piastres (1 EGP = 100 piastres) as BIGINT
JWT auth will be replaced by shared auth service in Project 2
Soft deletes used on users, transactions, transfers, scheduled_transfers
Double-entry bookkeeping: every transfer creates two transaction records
Webhook signature must be verified using stripe.ConstructEvent
/users/search requires a minimum query length of 3 characters and returns only non-sensitive fields (id, full_name, email)
Use SELECT ... FOR UPDATE SKIP LOCKED in the cron job to prevent concurrent duplicate execution across multiple instances