MalikNode System Architecture & API Design
Detailed blueprint of our file systems directories, containerized AdGuard Home isolation, WireGuard multi-subnet interfaces, and REST API communication workflows.
1. Directory Structure Organization
To keep tenant profiles, databases, and system configuration configurations cleanly organized, MalikNode resides in `/etc/maliknode` and `/var/www/html`:
/etc/maliknode/
├── central_api/ # Node.js / Python API Daemon Code
│ ├── config.json # SQLite DB paths, JWT signing keys, TLS settings
│ └── database.sqlite # Central accounts, subdomains, and device schemas
├── users/ # Tenant Configurations Folder
│ ├── user_abcd/ # Tenant "abcd" isolated workspace
│ │ ├── wg1.conf # WireGuard wg1 configuration file (Subnet: 10.88.1.0/24)
│ │ ├── keys/ # Peer private/public key pairs
│ │ └── adguard_data/ # Persisted volume mapping for user's AdGuard instance
│ └── user_efgh/
│ ├── wg2.conf # WireGuard wg2 configuration file (Subnet: 10.88.2.0/24)
│ └── adguard_data/
2. Subnet Separation & UDP Port Range Mapping (Oracle Cloud Ready)
Each tenant is allocated a completely independent subnet workspace. For instance:
- Interface: wg1
- Subnet Range: 10.88.1.0/24
- Tunnel Gateway: 10.88.1.1
- UDP Interface Port: 51821
- Interface: wg2
- Subnet Range: 10.88.2.0/24
- Tunnel Gateway: 10.88.2.1
- UDP Interface Port: 51822
How We Handle Unlimited Tunnels on Oracle Cloud (OCI):
Since OCI Security Lists restrict the number of individual port rules, we open a wide **UDP Port Range** in a single rule. This allows us to run up to 1,000 separate user subnets without needing to manually open ports:
- OCI Protocol: UDP
- Source CIDR: 0.0.0.0/0
- Destination Port Range: 51800-52800 (Opens 1,000 UDP ports in 1 rule!)
- Host OS Firewall Rule: sudo ufw allow 51800:52800/udp
3. Isolated AdGuard Instances (Docker)
Instead of sharing a single config dashboard, we instantiate **one lightweight Docker container of AdGuard Home per active client subnet**.
docker run -d \
--name adguard_user_abcd \
--restart unless-stopped \
-p 10.88.1.1:53:53/udp \
-p 127.0.0.1:3001:3000 \
-v /etc/maliknode/users/user_abcd/adguard_data:/opt/adguardhome/work \
adguard/adguardhome
By binding the DNS service port to 10.88.1.1:53 inside the WireGuard interface, Tenant A resolves their query lists in complete isolation without mixing records with Tenant B.
4. Server Load & Storage Capacity Analysis
- Max Tunnels: ~150 concurrent active handshakes
- WireGuard footprint: Virtually 0% CPU
- RAM per AdGuard: ~35 MB
- Expected max capacity: 100 users before RAM compression
- AdGuard Binary size: ~30 MB
- Query log rotation limit: 24 Hours / 10MB cap
- Storage per user: ~45 MB total
- Disk load: No additional volumes required
4.5. Production Database Setup (Phase 1 Complete)
To handle concurrent API queries, track cumulative data metrics, and support instant dashboards charts, we deployed a local PostgreSQL database cluster.
PostgreSQL Database Schema
-- 1. Users Table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
plan VARCHAR(20) NOT NULL,
subdomain VARCHAR(100) UNIQUE NOT NULL,
interface_name VARCHAR(10) UNIQUE NOT NULL,
listen_port INTEGER UNIQUE NOT NULL,
subnet_cidr VARCHAR(50) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. Peers Table
CREATE TABLE peers (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
device_label VARCHAR(50) NOT NULL,
vpn_ip VARCHAR(50) NOT NULL,
private_key VARCHAR(255) NOT NULL,
public_key VARCHAR(255) NOT NULL,
mesh_routing BOOLEAN DEFAULT FALSE,
adguard_client_id VARCHAR(100),
dns_filtering_policy VARCHAR(20) DEFAULT 'standard',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_device UNIQUE (user_id, device_label)
);
-- 3. Bandwidth Metrics Table (Real-Time Speed Graphs)
CREATE TABLE bandwidth_stats (
id BIGSERIAL PRIMARY KEY,
peer_id INTEGER REFERENCES peers(id) ON DELETE CASCADE,
tx_bytes BIGINT DEFAULT 0,
rx_bytes BIGINT DEFAULT 0,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
5. Wildcard Subdomain & DNS Configurations
You do not need to register subdomains manually inside Name.com when users sign up. We configure a Wildcard record to automate routing:
When a request hits `abcd.maliknode.app`, Name.com automatically maps it to your Nginx gateway. Nginx extracts the host header and serves the corresponding client folder or proxy interface.
6. Horizontal Scaling & DigitalOcean Droplet Deployment
When scaling your user capacity, you can link your primary 1-Core VPS with your secondary 3-Core / 18GB RAM VPS. This creates a highly secure, distributed cluster topology that prevents configuration changes or database load from interfering with active WireGuard UDP tunnels.
• Nginx TLS Gateway • Accounts SQLite DB • API Daemon Port 8085
• 500+ WireGuard Tunnels • Docker AdGuard Containers
Step-by-Step Backend Scaling Execution:
1. Provision droplet via DigitalOcean Portal
Deploy a new Ubuntu Droplet inside your existing DigitalOcean Project. In the networking selection tab, ensure the **VPC Network** checkbox is enabled. This assigns your droplet a private IP address within the `10.1.x.x` subnet pool, allowing secure local communication with your primary VPS without routing traffic over the public internet.
2. Install WireGuard & Docker on the Droplet
Connect to your new droplet and run the server provisioning dependencies:
sudo apt update && sudo apt install -y wireguard docker.io
sudo systemctl enable --now docker
3. Set Up Private API Communication Forwarding
On your primary Frontend VPS, configure your central API daemon config to talk directly to the Droplet IP. E.g. instead of running local shell scripts, the Frontend API daemon uses SSH keys or lightweight private HTTPS tokens to trigger configuration creations on the Worker VPS:
// config.json on Frontend VPS
{
"database": "./database.sqlite",
"worker_node": "10.1.20.15", // Droplet Private IP
"worker_ssh_key": "/etc/maliknode/keys/worker_ssh_id"
}
7. API Communication Workflows
When a user makes edits in their browser dashboard, transactions follow structured flows:
Workflow A: Signup / Provisioning
- Customer clicks select plan and posts data payload to Central Daemon.
- API assigns interface number, generates cryptographic keys, and writes local interface parameters.
- API spins up an isolated Docker container for the user's DNS server.
- API returns success callback containing their active subdomain (`musab.maliknode.app`).
8. Live API Daemon Deployment (Phase 2 Completed)
We deployed the live production API daemon using a Python **FastAPI** service managed by systemd, running on port **`8086`** to run alongside the existing stats server.
- **Shell Injection Shielding:** Command operations use system argument arrays (`subprocess.run(["wg", "syncconf", ...])`) without shell expansion (`shell=False`) to make arguments immune to user-injected characters.
- **JWT Auth Signatures:** Endpoints require secure JWT Bearer authorization headers to sign off admin tasks.
- **Non-Blocking Reloads:** Creating or deleting clients reloads configurations via `wg syncconf` without dropping active tunnels or breaking other users' tunnels.
9. Phase 3 Integration Blueprint (Frontend <-> API)
Phase 3 focuses on replacing the static mockup logic inside your Client Dashboard page with real-time `fetch()` HTTP queries targeting your FastAPI daemon. Below is the operational integration script blueprint:
On dashboard load, the browser checks for a saved JWT Token. If missing, it displays the login card overlay:
async function handleLogin(username, password) {
const res = await fetch("https://api.maliknode.app/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.status === "success") {
localStorage.setItem("mn_token", data.token);
window.location.reload();
}
}
Fetches and maps connected devices to your peer table and mesh visualization network dynamically:
async function fetchUserPeers() {
const token = localStorage.getItem("mn_token");
const res = await fetch("https://api.maliknode.app/api/peers", {
headers: { "Authorization": `Bearer ${token}` }
});
const data = await res.json();
if (data.status === "success") {
renderPeersTable(data.peers); // Injects real rows into your table
drawMeshNetwork(data.peers); // Lights up SVG lines for active peers
}
}