Documentation

Internal Accounts

Named username/password accounts a tenant provisions itself for its own known visitors — students, staff, residents, patients — with per-account data caps, durations, device limits, speed caps, fair-usage overrides, voucher issuance, and unit/room/ward tagging.

1

What an internal account is, and who it's for

An internal account (tenant_internal_accounts in the database) is a real, permanent username/password a tenant creates itself for one specific person it already knows — a student, a staff member, a resident, a patient, a ward. It is the deliberate alternative to a single shared WiFi password that inevitably leaks past the people it was meant for: each account is its own identity, with its own data cap, its own duration, and its own device limit, so one login can't quietly become the whole building's free WiFi.

This is the "bring your own known visitors" access mode. It's distinct from every other way a guest gets online on this platform:

Access typeWho it's forHow they get online
Internal accountA known person the tenant itself lists (student, staff, resident, patient)Username + password the tenant set, or auto-provisioned via SSO on first sign-in
Anonymous guestAnyone walking in off the street, never pre-listed by the tenantSelf-registers with phone/email, then pays (voucher/data plan), watches an ad, or answers a survey
SubscriberA regular paying customer on a recurring ISP-style planSame recognized-guest login as any guest, but billed automatically each cycle once a card is on file
SSO accountSame population as internal accounts (students/staff), at an org with its own identity providerSigns in with their existing Google Workspace / Microsoft Entra ID / Okta / OIDC / SAML login — the platform never stores their password

See panel 6 below for the full distinction, including why SSO is really just a second way to run internal accounts, not a separate access mode.

2

Adding and managing accounts

From Free access → Internal Accounts on the Captive Dashboard, an admin adds one account at a time ("Add an account": username, password, data cap or unlimited, duration, max concurrent devices, speed caps, unit/room/ward label, FUP override) or generates a batch — up to 500 accounts in one request, usernames auto-generated from a prefix (e.g. student1234) with a random password for each, shown once in a downloadable table/CSV and never retrievable again afterward.

A username can't look like an email address or a phone number (student047 is fine, 0821234567 or name@mail.com is rejected) — this keeps the internal-account table cleanly separate from guest self-registration, which does use phone/email as the identifier. Usernames only have to be unique among currently-active accounts (a unique index scoped to status = 'active'), so a disabled account's username — a room number like "4a2" reused the next tenant year — can be reissued to someone new without colliding.

Editing an existing account ("Edit settings") changes its data cap, duration, device limit, speed caps, unit label, and FUP override — username and password are left alone; "Reset password" is a separate action for changing the password specifically. A bulk-edit ("Edit selected accounts") applies the same shared settings to up to 500 selected accounts at once, each independently re-checked against the caller's own tenant before being touched. "Disable" is an immediate kill switch: beyond flipping the row to disabled, it clears the account's currently-authorized devices and in-memory access grant right away, rather than just blocking future logins — a lost/stolen credential or a resident who's moved out stops working immediately, not whenever their current session happens to expire.

3

Internal-account voucher settings

Separately from an admin provisioning accounts directly, an already-active internal account holder can generate a one-time voucher for a visitor of their own (e.g. a resident issuing a WiFi voucher to a guest in their room). This is gated and shaped by a dedicated set of tenant-wide settings, all stored on the tenants table:

FieldDefaultWhat it controls
internal_voucher_creation_enabledoffMaster switch — if off, the whole feature is unavailable regardless of the other fields.
internal_voucher_bundle_mb500 MBData each generated voucher carries (ignored if unlimited data is on).
internal_voucher_unlimited_dataoffIssue unlimited-data vouchers instead of a fixed MB bundle.
internal_voucher_max_redemptions1How many times a single generated voucher code can be redeemed.
internal_voucher_max_per_day1How many vouchers one internal account can generate per calendar day.
internal_voucher_window_start / _window_endunset (no curfew)A time-of-day window (e.g. 08:00–20:00) outside which voucher creation is refused — "Vouchers can only be created between X and Y."
internal_voucher_down_kbps / _up_kbpsunset (inherits tenant default)Speed cap applied to the issued voucher's bundle.
internal_voucher_fup_enabledonWhether the older, separate resident-voucher fair usage mechanism below applies to vouchers issued this way.
internal_voucher_fup_modesoftsoft (throttle) or hard (cut off) once the threshold is crossed.
internal_voucher_fup_threshold_gb20 GBMonthly usage threshold before this FUP mechanism kicks in.
internal_voucher_fup_throttle_mbps2 MbpsSpeed throttled to in soft mode once over threshold.
internal_voucher_fup_sms_enabledonWhether an SMS warning/throttle notice fires for this mechanism.

Where each field lives in the dashboard. Free access, Owner & guest access, Guest vouchers from residents, then Save voucher settings. In the dashboard the fields are called: Allow residents to create guest vouchers (internal_voucher_creation_enabled), Unlimited data, Data cap per voucher (MB), Max redemptions per voucher (1 to 10), Max vouchers per resident per day (1 to 50), Download and Upload speed cap (Mbps, 0 means use the default guest speed), and Curfew starts and Curfew ends. The fair-usage fields underneath are Enable fair usage policy, When the limit is reached (Soft or Hard), Monthly limit (GB), Throttled speed (Mbps) and the SMS heads-up tick box.

What the account holder sees. On the captive portal, in My Usage, an active internal account gets a Create a voucher for a guest card when the master switch is on. Create voucher calls POST /api/isn/internal-voucher/create, which re-checks that the signed-in identifier is an active internal account of this tenant before it makes anything, then returns the code, the data amount and the access time. The voucher is free (price 0), lasts 24 hours or until the curfew end time that day, and allows the visitor's own extra devices up to the redemption limit. The card also lists the last 20 vouchers the account created (GET /api/isn/internal-voucher/history, newest first) with a plain status: Not redeemed yet, In use (n/max devices), Fully redeemed, Voided or Curfew ended. The daily limit counts a South African calendar day, and Hard fair usage refuses new vouchers once the month's limit is used. SSO-provisioned accounts count as internal accounts here too.

This resident-voucher FUP mechanism is explicitly a separate, older, narrower system from the account-holder's own fair usage policy (panel 4) — it's scoped to a calendar-month rollup of vouchers a resident issues to others, not to the resident's own direct logins. Code comments in the server are explicit that the two "collide" conceptually but are gated by different settings and must not be confused.

4

Fair usage policy (FUP) and the periodic-reset fix

Every internal account can set its own throttle threshold and speed (fup_threshold_gb, fup_throttle_down_mbps, fup_throttle_up_mbps on the account row) overriding the tenant-wide Universal FUP, just for that account's own direct logins. Leaving these blank falls back to the tenant default.

The bug that used to exist: an internal or SSO-provisioned account keeps ONE bundle for its entire validity window (it isn't reissued a fresh bundle the way a guest voucher is) — a 2-month or "never expires" account's usage could therefore only ever climb. Once that usage crossed the FUP threshold, the account would stay throttled permanently, because nothing ever reset the counter back down. The same gap applied to SSO-provisioned accounts, since an SSO login is just an internal account under the hood. Resident/roaming accounts' own FUP mechanisms were unaffected by this specific bug.

How it works now: two new tenant-level settings, internal_fup_reset_mode (null / rolling / calendar_monthly) and internal_fup_reset_days (1–3650, default 30), control a periodic reset. Off by default (null/"never") so existing tenants see no behavior change unless they opt in. The server function _internalAccountFupResetBaselineMB tracks a fup_period_started_at timestamp and fup_period_baseline_mb on the account's active bundle row (recent_bundles): in rolling mode, a new period starts every internal_fup_reset_days days from when the current period began; in calendar_monthly mode, a new period starts the moment the UTC calendar month changes. When a period elapses, the account's usage-so-far becomes the new baseline, so the FUP calculation (actual usage minus baseline) drops back to zero without touching the account's real data-allowance bookkeeping (used_mb/remaining_mb keep accruing normally — only the FUP math is affected). A fresh login after a reset restores the account to exactly the download/upload speed it would get on a brand-new grant, resolved through the same account-override-else-tenant-default chain used at grant time, rather than guessing.

Set the reset mode and rolling period from Internal Accounts → Fair usage policy reset on the Captive Dashboard (fields internal-fup-reset-mode / internal-fup-reset-days, saved via POST /api/isn/bypass-settings, the same settings endpoint the owner-bypass and open-access tenant settings use). Turning the mode back off discards any stale baseline instead of continuing to apply it — it returns to counting true lifetime usage.

5

Unit / room / ward tagging

Each account can carry an optional free-text unit_label (up to 60 characters) — "Ward 4A", "Room 212", "Block C" — set when adding or editing an account. It is purely a label the admin types; there is no automatic physical-location detection behind it, so it only ever means what was typed in. Multiple internal accounts can legitimately share the same unit label (several residents in one room, several patients on one ward).

The accounts list can be filtered by this label: the "Filter by unit" field above the table (ia-unit-filter) narrows GET /api/isn/internal-accounts with a unitLabel query parameter, letting a hospital or student residence see everyone tied to one physical unit at a glance without running a separate router or tenant per room.

6

How this differs from a Subscriber, an anonymous guest, and SSO

An internal account is a known person the tenant itself lists in advance and hands credentials to — the tenant brings its own population. An anonymous guest is the opposite: anyone who walks in and self-registers with no prior relationship to the tenant, then earns or buys access per-session via a voucher, data plan, ad watch, or survey — the tenant never typed their name into anything beforehand. A Subscriber is a guest (the same self-registered identity, or one a cash-paying customer already has) upgraded onto a recurring ISP-style billing plan — they log in exactly like any other recognized guest, with no separate password, and are simply charged automatically each cycle once a card is on file; a subscriber is a billing relationship layered onto an existing identity, not a new kind of login credential the way an internal account is. SSO is not a fifth category at all — it's a second way to populate the internal-accounts table: instead of an admin typing in a username and password by hand, an account is auto-provisioned (added_via = 'sso') the first time someone authenticates against the tenant's own OIDC/SAML identity provider, using the same fields (data cap, duration, device limit, speed caps, FUP override) an admin would set manually, just applied automatically from the SSO defaults at the moment of first sign-in. No password for an SSO-provisioned account is ever stored on this platform.

7

Edge cases and limits worth knowing

ItemDetails
Device slots only count while actually connected A device is freed from the account's max_concurrent_devices count roughly 10 minutes after it's no longer seenNeither the captive portal nor (where supported) the router's own device list reports it — not for the rest of whatever duration window the account was granted. A 5-minute visit under a 24-hour grant doesn't lock that slot for the remaining time.
Duration presets vs. an exact expiryPreset durations (2 hours, 24 hours, 2 months, 1 year, never expires) are a rolling window that restarts from "now" each time a previous grant has fully lapsed. duration_mode = 'custom' pins the account to one literal calendar deadline (custom_expires_at) instead — useful for a lease end date or a semester end — and once that date passes the account is permanently, automatically locked out with no rolling restart.
Automatic expiry sweepA background job (_runInternalAccountExpirySweep, running every 15 minutes plus once at server startup) disables any custom-mode account whose deadline has passed and flips its DB status to disabled. This deliberately only touches custom-mode accounts — a preset-mode account has no calendar deadline to sweep on. Without this sweep, an expired account's login correctly still gets refused at connect time, but its phone/email would otherwise stay permanently "taken" from the guest-registration availability check, blocking that person from ever signing up fresh as an ordinary guest with the same number.
Logging back in doesn't re-grant a fresh bundle A capped (non-unlimited) account that logs out and back in resumes its existing active bundle rather than being handed a brand-new allocation each timeFixed as a live-reported bug where repeated logout/login was quietly re-issuing full data every time.
Static IP is per-account, no extra toggle Unlike a Subscriber plan (which needs "Reserve a static IP for subscribers on this plan" switched on first), any internal account can have "Assign IP" used directlyAssigning one IS the gate. It pushes a real DHCP static-lease command to the router (MikroTik and OpenWRT today) and is a LAN-side reservation only, not a public IP or PPPoE tunnel.
Batch-generated passwords are shown exactly once They're returned in the creation response and downloadable as CSV/Excel immediately after, then never retrievable againOnly a password reset can set a new one afterward.
Max concurrent devices is capped 1–20Per account, and batch generation is capped at 500 accounts per request (bulk-edit is capped the same way).