Most MikroTik deployments start with RouterOS's built-in User Manager: an admin clicks "Hotspot → Users", types a username and password, and hands a printed slip to a customer. That workflow holds up for a handful of vouchers a day. It collapses the moment an ISP is selling hundreds of hotspot codes a day across multiple sites — every router needs to be touched individually, expiry has to be tracked by hand, and there's no single place to see which vouchers are still unused.

The fix is to stop treating the router as the source of truth for who's allowed online. Instead, an external RADIUS server becomes the authority, and a Spring Boot application becomes the system that creates, batches, and tracks vouchers against it. RouterOS already speaks RADIUS natively for both Hotspot and PPPoE — you're not bolting on anything exotic, you're just pointing it at a server you control instead of its internal table.

The authentication handshake

When a hotspot client submits a voucher code on the captive login page, RouterOS sends a standard RADIUS Access-Request to the configured NAS (Network Access Server) endpoint, carrying the username, password (the voucher code itself, usually identical to the username for single-use vouchers), and the router's shared secret. The RADIUS server checks the credential against its radcheck table and replies with either Access-Accept or Access-Reject.

The interesting part happens inside the Access-Accept. RADIUS lets the server attach reply attributes that RouterOS will honor immediately:

  • Session-Timeout — hard-caps the session length in seconds, so a 1-hour voucher can't be reused indefinitely by reconnecting.
  • Mikrotik-Rate-Limit — sets upload/download speed in RouterOS's native format (e.g. 2M/5M), mapping the voucher directly to a sellable package tier without touching the router's queue tree.
  • Acct-Interim-Interval — tells the router how often to send interim accounting updates, which is what lets the billing system track live data usage instead of waiting for the session to end.

None of this requires logging into the router. A voucher's speed, duration, and validity window are entirely a function of what rows exist in the RADIUS database at the moment of authentication.

Schema and batch generation

On the application side, a voucher batch is a straightforward entity: code, packageId, status (UNUSED / ACTIVE / EXPIRED / REVOKED), validFrom, validUntil, and the tenantId it belongs to (since a single platform instance serves many ISPs). Generating a batch of 500 vouchers is a transaction that:

  1. Generates N unique alphanumeric codes (collision-checked against existing codes for that tenant).
  2. Inserts one radcheck row per code with a Cleartext-Password check attribute equal to the code.
  3. Inserts the matching radreply rows derived from the selected package (rate limit, session timeout).
  4. Persists the billing-side Voucher row with status UNUSED, ready to be printed or sent as an SMS code.

Steps 2–4 happen inside the same database transaction as step 1 to guarantee a voucher never exists in the billing system without a matching RADIUS identity, or vice versa — a mismatch there is the single most common cause of "the voucher just doesn't work" support tickets.

Reboot and re-authentication edge cases

RouterOS doesn't persist hotspot session state across a reboot the way a RADIUS-backed flow needs it to. Three behaviors are worth designing around explicitly:

  • Stale sessions after a power cut. If a router loses power mid-session, RADIUS never receives an Accounting-Stop. The billing system should treat any session with no stop record and a last interim update older than roughly 2× the interim interval as abandoned, and close it out on a schedule rather than waiting indefinitely.
  • Re-authentication on reconnect. When the client's device reconnects after the reboot, RouterOS will send a fresh Access-Request for the same voucher. The server has to decide whether remaining session time should resume (track cumulative Acct-Session-Time against the original Session-Timeout) or whether a single login consumes the whole voucher regardless of disconnects — this is a product decision, not a technical one, and it needs to be consistent or customers will dispute charges.
  • Clock drift. If the router's NTP isn't configured, its local clock can drift enough to make a voucher appear expired prematurely or stay valid past its window. Mandating NTP sync as part of router provisioning avoids an entire category of "expired too early" complaints.

Common gotchas

A few issues show up repeatedly across deployments:

  • NAS secret mismatches — the shared secret configured in RouterOS's Radius menu must match the nas table entry exactly, including no trailing whitespace; a mismatch fails silently with generic Access-Reject logs that give no hint as to the cause.
  • Duplicate Calling-Station-Id — some captive portal flows reuse a MAC address across devices behind NAT; if voucher logic keys uniqueness off MAC instead of the voucher code itself, legitimate second devices get rejected.
  • Accounting packets dropped silently — if the accounting port (1813) isn't open on the same firewall rule as authentication (1812), sessions authenticate fine but usage tracking and live "active users" dashboards stay empty, which looks like a billing bug but is actually a network ACL issue.

Once this loop is wired correctly, voucher generation becomes a five-second admin action instead of a router-by-router chore, and the same RADIUS plumbing carries straight over to PPPoE subscriber authentication with no separate integration work.