Skip to content

Database Schema

ProxPanel uses PostgreSQL 16 as its single source of truth. The full DDL lives in backend/internal/models/schema.sql and is applied at startup (idempotent — CREATE TABLE IF NOT EXISTS everywhere). This page summarises the most important tables, their key columns, indexes, and soft-delete status.

Conventions used below:

  • PK — primary key.
  • FK → table — foreign key.
  • soft-delete — table has a deleted_at TIMESTAMP column; rows are not removed, only marked.
  • All tables include created_at / updated_at unless noted.
  • Columns marked [ENC] are encrypted at the application layer (AES-256-GCM with license-derived key).

The central object — one PPPoE / IPoE end customer.

ColumnTypeNotes
idSERIALPK
usernameVARCHAR(100)PPPoE login — unique via partial index WHERE deleted_at IS NULL
passwordVARCHAR(255)bcrypt hash for portal login
password_plainVARCHAR(255)[ENC] PPPoE password (encrypted)
full_name, email, phone, address, region, building, nationality, country, noteCustomer-info fields
service_idINTEGERFK → services
statusINTEGER1=active, 0=inactive
expiry_date, due_dateTIMESTAMPLifecycle dates
price, override_priceDECIMAL / BOOLPer-subscriber price override
auto_renew, auto_recharge, auto_recharge_daysAuto-renewal config
daily_quota_used, monthly_quota_usedBIGINTBytes; reset by daily / monthly resetters
daily_download_used, daily_upload_used, monthly_download_used, monthly_upload_usedBIGINTDirection split
fup_level, monthly_fup_levelINTEGER0=normal, 1..6=tiers
last_daily_reset, last_monthly_reset, last_quota_reset, last_quota_syncTIMESTAMPReset bookkeeping
last_session_download, last_session_uploadBIGINTQuotaSync delta baseline
last_bypass_cdn_bytesBIGINTCDN bypass counter
mac_addressVARCHAR(50)MAC binding
ip_addressVARCHAR(50)Currently-online IP
static_ipVARCHAR(50)Reserved IP if any
save_macBOOLLock on first connect
nas_id, switch_idINTEGERFK → nas_devices / switches
reseller_idINTEGERFK → resellers — required
collector_idINTEGERFK → users (optional)
latitude, longitudeDECIMALMap coords
is_online, last_seen, session_id, simultaneous_sessionsSession state
whatsapp_notifications, wan_check_status, port_openComms / WAN
deleted_by_id, deleted_by_nameAudit trail for archived
deleted_atTIMESTAMPSoft-delete

Indexes: (mac_address), (is_online), (reseller_id), (service_id), (nas_id), (status), (username), (expiry_date), (deleted_at), plus (last_seen) WHERE is_online=true (v1.0.388).

Operator accounts — admins, resellers, support, collectors, read-only.

ColumnTypeNotes
idSERIALPK
usernameVARCHAR(100)Unique
passwordVARCHAR(255)bcrypt
password_plainVARCHAR(255)[ENC] — visible to admins only
email, phone, full_nameContact
user_typeINTEGER1=admin, 2=reseller, 3=support, 4=collector, 5=read-only
is_activeBOOLLogin enabled
force_password_changeBOOLPrompt on next login
two_factor_enabled, two_factor_secretTOTP
reseller_idINTEGERFK → resellers (NULL for admins)
permission_groupINTEGERFK → permission_groups
last_loginTIMESTAMPAudit
deleted_atTIMESTAMPSoft-delete

The hierarchy node above subscribers.

ColumnTypeNotes
idSERIALPK
user_idINTEGERFK → users — 1:1
nameVARCHAR(255)Display name
balanceDECIMAL(15,2)Available funds
creditDECIMAL(15,2)Credit limit (allows balance to go negative up to this)
addressVARCHAR(500)
parent_idINTEGERFK → resellers — sub-reseller tree
permission_groupINTEGERFK → permission_groups
bandwidth_rule_idINTEGEROptional reseller-level bandwidth rule
is_activeBOOLDisable reseller
deleted_atTIMESTAMPSoft-delete

Named bundles of permissions.

ColumnTypeNotes
idSERIALPK
nameVARCHAR(100)Unique
descriptionVARCHAR(255)

The 220 canonical permission rows.

ColumnTypeNotes
idSERIALPK
nameVARCHAR(100)Unique — category.action
descriptionVARCHAR(255)Human-readable

M:N join.

ColumnTypeNotes
permission_group_idINTEGERFK ON DELETE CASCADE
permission_idINTEGERFK ON DELETE CASCADE
PKcomposite(group, permission)

The plan catalogue. ~50+ columns — only the load-bearing ones shown.

ColumnTypeNotes
idSERIALPK
name, commercial_name, descriptionIdentity
download_speed, upload_speedBIGINTkbps
download_speed_str, upload_speed_strVARCHARe.g. 2000k, fed to MikroTik
burst_download, burst_upload, burst_threshold, burst_timeRouterOS burst config
soft_fup_enabledBOOLWhether FUP shapes vs cuts
daily_quota, monthly_quotaBIGINTBytes — 0 = unlimited
time_quotaINTEGERTime-based quota seconds
fup1_threshold .. fup6_thresholdBIGINTDaily FUP step bytes
fup1_download_speed .. fup6_upload_speedBIGINTSpeeds for each step
monthly_fup1_* .. monthly_fup6_*Same for monthly
price, day_price, reset_priceDECIMALPricing
expiry_value, expiry_unit, entire_month, monthly_accountRenewal period
time_from_hour, time_from_minute, time_to_hour, time_to_minuteFree-hours window
time_download_ratio, time_upload_ratioINTEGER% of usage discounted during window
nas_idINTEGERFK → nas_devices (preferred NAS)
pool_nameVARCHAR(100)Framed-Pool to send
address_list_in, address_list_outVARCHARMikroTik address-list names
queue_typeVARCHAR(50)simple, pcq
time_based_speed_enabledBOOLToggle the free-hours feature
is_active, sort_orderList ordering
deleted_atTIMESTAMPSoft-delete

Per-reseller price override per service.

ColumnTypeNotes
idSERIALPK
reseller_id, service_idINTEGERUnique pair
price, day_priceDECIMALOverride values
is_enabledBOOLHide the service from this reseller if false

Per-service subscriber caps.

ColumnTypeNotes
reseller_id, service_idINTEGERUnique pair
max_subscribersINTEGERCap

BNGs / routers ProxPanel talks to.

ColumnTypeNotes
idSERIALPK
name, short_name, descriptionIdentity
ip_addressVARCHAR(50)Unique
typeVARCHAR(50)mikrotik (default) / cisco / juniper / huawei
secretVARCHAR(100)RADIUS shared secret — hidden from API responses
auth_port, acct_port, coa_portINTEGERRADIUS ports
api_username, api_passwordMikroTik API creds (api_password [ENC])
api_port, api_ssl_port, use_ssl, ftp_portAPI endpoint
subscriber_pools, allowed_realmsVARCHAR(500)Comma-separated whitelists
is_active, is_online, last_seen, version, active_sessions, total_usersHealth / telemetry
deleted_atTIMESTAMPSoft-delete

Hierarchy device above a NAS (for the topology diagram).

ColumnTypeNotes
idSERIALPK
name, location
parent_idINTEGERFK → switches (self-ref tree)
deleted_atTIMESTAMPSoft-delete

These follow the FreeRADIUS schema convention (lowercase, no underscores in column names) so they remain interchangeable with the upstream tool ecosystem.

Per-username check attributes (cleartext password, expiry).

ColumnTypeNotes
idSERIALPK
usernameVARCHAR(64)Indexed
attributeVARCHAR(64)e.g. Cleartext-Password
opCHAR(2):=
valueVARCHAR(253)

Per-username reply attributes (Framed-IP-Address, Mikrotik-Rate-Limit).

ColumnTypeNotes
idSERIALPK
usernameVARCHAR(64)Indexed
attributeVARCHAR(64)e.g. Framed-IP-Address, Mikrotik-Rate-Limit
opCHAR(2):=
valueVARCHAR(253)
Composite index(username, attribute)

radgroupcheck / radgroupreply / radusergroup

Section titled “radgroupcheck / radgroupreply / radusergroup”

Group-based attributes. Rarely used by ProxPanel — kept for compatibility.

Accounting — one row per session.

ColumnTypeNotes
radacctidBIGSERIALPK
acctsessionidVARCHAR(64)Per-session ID from BNG
acctuniqueidVARCHAR(32)Unique across all NAS
username, realmSubscriber identity
nasipaddress, nasportid, nasporttypeNAS metadata
acctstarttime, acctupdatetime, acctstoptimeTIMESTAMPSession lifecycle
acctinterval, acctsessiontimeINTEGERDuration accounting
acctauthenticVARCHAR(32)RADIUS/Local
connectinfo_start, connectinfo_stopFree-form
acctinputoctets, acctoutputoctetsBIGINTBytes
calledstationid, callingstationidVARCHAR(50)NAS / MAC
acctterminatecauseVARCHAR(32)Why disconnected
servicetype, framedprotocolPPP / IPoE
framedipaddress, framedipv6address, framedipv6prefix, framedinterfaceid, delegatedipv6prefixAssigned addresses

Indexes: (username), (acctstarttime), (acctstoptime), (acctsessionid, username), (nasipaddress), (framedipaddress).

When the table exceeds ~10M rows, run the radacct partitioning runbook.

Post-auth audit (failed authentications, password attempts).

ColumnTypeNotes
idSERIALPK
usernameVARCHAR(64)Indexed
passVARCHAR(64)What the user tried
replyVARCHAR(32)Access-Accept / Access-Reject
callingstationidVARCHAR(50)MAC
authdateTIMESTAMPIndexed
classVARCHAR(64)

Money movements. See Transaction Types.

ColumnTypeNotes
idSERIALPK
typeVARCHAR(50)Indexed — see Transaction Types reference
amountDECIMAL(15,2)Signed
balance_before, balance_afterDECIMAL(15,2)Reseller balance snapshots
descriptionVARCHAR(500)
old_service_name, new_service_name, service_namePlan-change provenance
reseller_idINTEGERFK — indexed
subscriber_idINTEGERFK (NULL on transfer) — indexed
target_reseller_idINTEGERFor transfer type
ip_address, user_agentAudit metadata
created_byINTEGERFK → users
created_atTIMESTAMPIndexed

Not soft-deleted — transactions are immutable history.

Professional invoice records.

ColumnTypeNotes
idSERIALPK
invoice_numberVARCHAR(50)Unique
subscriber_id, reseller_idINTEGERFK — indexed
sub_total, discount, tax, total, amount_paidDECIMAL(15,2)Money
statusVARCHAR(20)pending, completed, failed, refunded, rolled_forward — indexed
due_date, paid_dateTIMESTAMPLifecycle
public_tokenVARCHAR(64)Random token used by /api/invoices/public/:id so QR codes are not enumerable
billing_period_start, billing_period_endTIMESTAMP
auto_generatedBOOLFrom a scheduler vs created manually
notification_logJSONBTimeline of notifications sent
rolled_forward_to_invoice_idINTEGERFK → invoices (if unpaid balance was rolled into a new invoice)
deleted_atTIMESTAMPSoft-delete

Line items on an invoice.

ColumnTypeNotes
idSERIALPK
invoice_idINTEGERFK
descriptionVARCHAR(255)
quantity, unit_price, totalMoney

Operator-recorded payments (often collector cash entries).

ColumnTypeNotes
idSERIALPK
invoice_id, subscriber_id, reseller_id, collector_idINTEGERFKs
amountDECIMAL(15,2)
methodVARCHAR(50)cash, card, online
reference, notesFree-form
statusVARCHAR(20)Defaults completed

Voucher cards.

ColumnTypeNotes
idSERIALPK
codeVARCHAR(50)Unique
pinVARCHAR(20)Optional
service_id, reseller_idINTEGERFK
value, days, quota_refillCard contents
is_used, used_by, used_atRedemption state
is_active, expiry_dateCancellable / expirable
batch_id, batch_numberPrint batch tracking

Pricing catalogue and per-subscriber rentals for static IPs.

ColumnTypeNotes
idSERIALPK
name, price, days, is_activeCatalogue entry
(rental) subscriber_id, ip_address, price_id, start_date, expiry_date, is_active, deleted_atPer-subscriber row

Assignments of subscribers to collectors for cash collection runs.

ColumnTypeNotes
collector_id, subscriber_id, reseller_id, invoice_idFKs
statusVARCHAR(20)pending, collected, failed, cancelled
auto_renew, send_notification, amount, notes, collected_at, payment_id, created_by

Append-only operator-action history.

ColumnTypeNotes
idSERIALPK
actor_idINTEGERFK → users (the operator)
actor_nameVARCHAR(100)Denormalised for performance
actionVARCHAR(100)e.g. subscriber.update, reseller.add_money
entity_typeVARCHAR(50)subscriber, reseller, …
entity_idINTEGERTarget
entity_nameVARCHAR(255)Denormalised
descriptionTEXTHuman-readable summary
changesJSONBField-level diff (where applicable)
ip_address, user_agentRequest metadata
created_atTIMESTAMPIndexed

Scheduled backup definitions.

ColumnTypeNotes
idSERIALPK
name, backup_type, frequency, retentionIdentity + retention
day_of_week, day_of_month, time_of_dayWhen to run
storage_type, local_pathlocal / cloud / ftp
ftp_enabled, ftp_host, ftp_port, ftp_username, ftp_password, ftp_path, ftp_passive, ftp_tlsFTP target
last_status, last_error, last_backup_file, last_run_at, next_run_atLast run state
deleted_atTIMESTAMPSoft-delete

Per-backup execution record.

ColumnTypeNotes
idSERIALPK
schedule_id, schedule_nameOrigin
filename, storage_path, storage_typeWhere it landed
backup_type, duration, created_by_id, created_by_nameMetadata

Time-window-based speed adjustments.

ColumnTypeNotes
idSERIALPK
nameIdentity
service_idINTEGERIndexed — scope of the rule
time_from_hour, time_from_minute, time_to_hour, time_to_minuteWindow
download_percent, upload_percentINTEGERMultiplier
is_activeBOOLToggle — indexed
days_of_weekVARCHARComma-separated 0-6

Per-subscriber bandwidth override.

ColumnTypeNotes
idSERIALPK
subscriber_idINTEGERIndexed
rule_typeVARCHAR(20)internet, cdn
enabledBOOLToggle
download_speed, upload_speedINTEGERAbsolute kbps override
cdn_id, cdn_nameFor cdn rules
duration, expires_atTemporary rules
priorityINTEGERConflict resolution

CDN entries — named groups of IP subnets.

ColumnTypeNotes
idSERIALPK
name, descriptionIdentity
subnetsTEXTComma-separated CIDRs
is_activeBOOL

Port-based PCQ speed control.

ColumnTypeNotes
idSERIALPK
name, descriptionIdentity
port, protocolMatch
directionVARCHARsrc, dst, both, dscp
dscp_valueINTEGERWhen direction=dscp
download_speed, upload_speedINTEGERkbps
nas_idINTEGEROptional NAS target

Per-service CDN attachments (with FUP overrides).

ColumnTypeNotes
service_idINTEGERIndexed
cdn_idINTEGERFK
time_based_speed_enabledBOOL
pcq_* columnsPCQ settings on this attachment

Automated WhatsApp / SMS / email triggers.

ColumnTypeNotes
idSERIALPK
nameIdentity
trigger_eventVARCHAR(50)Indexed — expiry_warning, fup_applied, etc.
is_activeBOOLIndexed
channelsVARCHARComma-sep whatsapp,sms,email
template, subjectTEXTContent
filtersJSONBService / FUP-level / reseller filters

TTL-analysis output from the nightly scan.

ColumnTypeNotes
idSERIALPK
subscriber_idINTEGERFK
detected_atTIMESTAMPWhen found
ttl_anomaly, device_count_estimateFindings
auto_disconnectedBOOLWhether the user was disconnected

Registered TR-069 routers.

ColumnTypeNotes
idSERIALPK
serial_numberVARCHARUnique
manufacturer, model, oui, product_class, firmware_versionIdentity
ip_address, mac_addressNetwork
subscriber_idINTEGERFK — link to ProxPanel subscriber
wifi_ssid, wifi_password, wifi_channel, wifi_enabledCurrent WiFi state
connected_clientsINTEGERHosts behind the CPE
connected_hostsJSONBDetailed list of hosts
last_inform, last_connection_requestTIMESTAMPTR-069 lifecycle

Pending / completed TR-069 task queue.

ColumnTypeNotes
idSERIALPK
device_idINTEGERFK → cpe_devices
task_typeVARCHARget_parameter_values, get_hosts, set_wifi, reboot
payloadJSONBParameters
statusVARCHARpending, running, done, failed
resultJSONB

Mirrors MikroTik pool ranges. Prevents duplicate IP allocation across radreply + pool.

ColumnTypeNotes
idSERIALPK
ip_addressVARCHAR(15)Unique
pool_nameVARCHAR(64)Which pool the IP belongs to
statusVARCHAR(20)available, in_use
username, subscriber_id, nas_idAllocation metadata
assigned_atTIMESTAMP

Local cluster role + secret for this node.

ColumnTypeNotes
roleVARCHARstandalone, main, secondary
cluster_id, cluster_secretVARCHARIdentity
main_server_ipVARCHARWhen secondary

Registered cluster members (only present on main).

ColumnTypeNotes
idSERIALPK
cluster_id, node_id, role, ip_address, hostnameIdentity
last_heartbeat, status, cpu_usage, memory_usage, disk_usage, replication_lagTelemetry

Audit log of cluster events.

ColumnTypeNotes
event_typeVARCHARnode_joined, node_left, failover, manual_failover
node_id, details, created_at

HttpOnly-cookie refresh tokens.

ColumnTypeNotes
idSERIALPK
user_idINTEGERFK → users
token_hashVARCHARsha256 — only the hash stored
family_idUUIDToken-family for rotation
issued_at, expires_at, revoked_atTIMESTAMPLifecycle
user_agent, ip_addressAudit

Logout invalidation.

ColumnTypeNotes
token_hashVARCHARsha256 of the JWT
expires_atTIMESTAMPAuto-cleaned after expiry

Key / value store for runtime config (timezone, reset times, feature flags).

ColumnTypeNotes
keyVARCHAR(100)Unique
valueTEXT
updated_atTIMESTAMP

Per-tenant configuration.

ColumnTypeNotes
tenant_idVARCHARUnique
schema_nameVARCHARtenant_<id>
domain, branding, feature_flags

Tables with deleted_at:

  • subscribers
  • services
  • nas_devices
  • switches
  • resellers
  • users
  • invoices
  • static_ip_rentals
  • backup_schedules

All others are hard-delete (history tables like transactions, audit_logs, radacct are never deleted in normal operation; radacct is archived to radacct_archive).

ProxPanel uses schema.sql with idempotent statements rather than GORM AutoMigrate. Reasons:

  1. Predictable on upgrades — no auto-add-column surprises.
  2. Strict, predictable schema control.
  3. Easier rollback — diff the SQL files.

For column additions across released versions, the file uses ALTER TABLE ... ADD COLUMN IF NOT EXISTS after the relevant CREATE TABLE.