A full web interface to manage your Assetto Corsa server without touching the terminal. Accessible from any device on your network, or from anywhere in the world using Cloudflare Tunnel.
Live server metrics (CPU, RAM, uptime), AC server status, and a real-time log stream β all updated instantly via Server-Sent Events. Live position minimap: when the server is running and at least one driver is connected, the Dashboard renders the current track's map.png with one absolute-positioned dot per car at ~4 Hz, driven by acServer's UDP position stream β same worldβpixel projection (WIDTH / HEIGHT / X_OFFSET / Z_OFFSET / SCALE_FACTOR / MARGIN from data/map.ini) that Content Manager and CSP use, so the dots align with what drivers see in-game. The widget is hidden on an idle server and its SSE stream is ref-counted on subscribers, so an empty Dashboard tab doesn't drain CPU. Admin-only persistent "Clear logs" drops the in-memory buffer AND truncates AC_LOG_FILE on disk, then broadcasts an SSE clear event so every open tab wipes in lock-step β refreshing the page no longer brings the lines back. Activity card resolves car model IDs to their catalogue display name (Toyota AE86 instead of ks_toyota_ae86). Panel restarts are transparent to connected players β acServer is spawned in its own session via setsid(2) and writes straight to disk, so a systemctl restart of the panel no longer kicks anyone.
Live player table with car, lap count, best/last time and country flag. Kick and ban directly from the panel. Full history of every player who has ever joined the server. Per-player admin-set nicknames β pencil button in the history table opens a modal to attach a real name to an in-game alias; the panel then renders rows as "Nickname (in-game)" everywhere, including historic lap times.
Every lap time stored in SQLite automatically. Live ingest via UDP plugin β laps land in the database within milliseconds of crossing the finish line, no waiting for the session-end JSON dump. The panel auto-configures UDP_PLUGIN_LOCAL_PORT and UDP_PLUGIN_ADDRESS in server_cfg.ini on the first session apply (zero manual setup). Cross-source dedup via a content-based UNIQUE INDEX prevents the post-session JSON importer from duplicating laps the UDP listener already captured; the JSON instead fills in sector splits on those rows.
Three views: Records (best lap per driver+track), All laps (every row, paginated 10/page) and Compare drivers (side-by-side delta table for up to 4 drivers). Filter by track, car, date or validity. CSV export. Manual lap insert β admin-only "Add lap" popup writes directly into the same laps table the UDP listener uses (shared dedup index, lap.create audit entry, Discord-record notification reused), so a missed lap from a server outage or an external timing source can be backfilled without touching SQLite by hand.
Every driver the panel has seen gets a shareable URL at /p/<steam-id> β no login required. The page is server-rendered HTML with the panel's own design tokens (light/dark theme, accent colour, typography) and shows: hero card with avatar, nickname, in-game name, country flag (real flagcdn.com image, not unicode emoji) and a clickable Steam-chip linking to the public Steam profile; four KPI cards (total laps, time on track, best lap, server records held); a Server records table listing every (track, layout, car) combo where the driver holds the panel-wide MIN β track and layout names resolved through ui_track.json so they match the Active Session card down to the variant ("Grand Prix" instead of layout_gp); a Personal bests table across every combo the driver has driven; a Recent laps table with the last 10 laps including invalid/cuts annotations. A topbar with sun/moon theme toggle and EN/ES/IT flag switcher lets the visitor swap colour scheme or language β choice persists in the URL (?theme=light, ?lang=en) so the picked variant survives sharing. Full i18n on the SSR page (en/es/it) via Accept-Language detection with panel_settings.lang fallback. OpenGraph + Twitter card meta tags ship name + headline stats + a per-driver 1200Γ630 PNG card so a paste in Discord renders a proper preview with image β the OG image URL is versioned with BUILD_VERSION-lastSeen so Discord re-fetches automatically after deploys + new sessions. A download button in the page topbar exports a richer stat card to disk (/p/<guid>/card.png) with five KPI tiles including the driver's most-used car and best lap on their most-played track plus a silhouette of that track's map.png β light + dark variants follow whatever the page is currently showing. A companion GET /api/public/players/<steam-id> returns the same shape as JSON (with extra mostUsedCar, mostPlayedTrack, bestOnMostPlayed and recentLaps[] fields) for Discord bots / Twitch overlays. All public endpoints rate-limit at 120/min/IP, accept GET + HEAD, and respect the Settings β Public profiles admin toggle (default on; flipping it off turns the URLs into a generic 404 server-wide). A small share button on each row of the Players β Connection history table copies the public URL to clipboard with one click.
Browse all installed cars and tracks with images, specs and multi-layout support. Separate Kunos content / Mod content toggles β the Kunos toggle auto-flips off on first load when mods are present so modded servers don't drown the catalogue in stock content. Spinner-overlay on each thumbnail with fade-in once fully loaded, so heavy mod previews don't paint in visible chunks. Per-slot skin selection: the modal's "Add to slot" passes the selected skin to a slots array in session config β the same car can occupy multiple grid positions with different liveries (e.g. FK2 blue + FK2 red as two distinct slots), each landing as its own [CAR_n] block in entry_list.ini. Admin-only "Delete" button inside each modal removes the mod car/track folder recursively from /content/{cars,tracks} (audited as car.delete / track.delete); Kunos content is hard-refused server-side and the button is hidden for it, so the bundled DLC catalogue stays intact and continues serving as the asset-fallback source for the rest of the panel.
Per-session (Practice / Qualify / Race) enable toggles β when a session is disabled the panel physically removes its section from server_cfg.ini so LOOP_MODE only cycles the enabled ones. Independent duration/laps per session, weather and air temperature, time of day (hour 0..23 mapped to SUN_ANGLE), race penalties toggle. The entry_list.ini is regenerated automatically whenever the car set changes so acServer never refuses to boot on a stale [CAR_n].MODEL reference.
Save a whole sessionCfg (track, layout, slots with per-car/skin combinations, session toggles + durations, weather, time of day, penalties, max clients) under a name and load it back later in one click. Two creation paths from the Presets page: Save as preset on the Session page snapshots the live config, while New preset opens a self-contained builder that composes a configuration from scratch without touching the running session. The pencil icon on any card re-opens the same builder pre-filled, so editing is the same flow as creating. Loading a preset drops its config into the Session page and leaves the operator one click away from Apply β never reboots the running session by accident. Presets are also portable: every card has a download button that exports the preset as <name>.json, and the Import button accepts those files (or hand-crafted { name, description, config } JSON), auto-suffixing the name with (2), (3), β¦ when a preset already exists. Gated by the presetManage permission so non-admins can manage presets without also getting server_cfg.ini edit rights.
Upload mods as .zip, .rar or .7z straight from the browser. The server automatically detects whether it's a car or a track and installs it in the right folder. Works remotely too, with chunked upload support for Cloudflare and other proxies.
Edit server_cfg.ini through a visual interface: server name, ports, slots, passwords (with show/hide toggle), driving aids, whitelist and more. Race-rule and behaviour options persist correctly even at value 0. Country + city selector writes a [GEO_PARAMS] section in the canonical "<Name>, <ISO2>" format Content Manager and the acstuff lobby read to render the server flag β the panel creates the section on first save and leaves IP= blank so the lobby fills it from the registration packet (so dynamic-IP / NAT setups don't end up advertising a stale address). Requires an acServer restart to reach the lobby, same as any other server_cfg.ini change.
Create, edit and delete panel users. Each user has their own profile with password change and a built-in secure password generator (uses crypto.getRandomValues). The panel refuses to delete or demote the last remaining admin and revokes a user's active sessions when an admin resets their password.
The user role is gated by ten independent toggles edited from the Users page: serverControl, sessionEdit, serverConfig, presetManage, whitelistManage, playerModeration, modUpload, discordWebhook, auditView, dbBackup. Admin always passes every check. Defaults preserve the open grants from earlier deployments (server control / session edit / mod upload / preset management on; the rest off). Editing a permission takes effect on the affected user's next request β no re-login needed. New permissions added in a later release auto-backfill into pre-existing role rows with their intended defaults so upgrades don't silently demote users. Panel-user CRUD, AC PASSWORD / ADMIN_PASSWORD and the permission set itself are reserved to admins by design (cannot be exposed as toggles without enabling privilege escalation).
Drop a Discord webhook URL into Settings β Discord; the server posts a localized message every time a driver beats the previous best lap for a (track, layout, car) combination on the live UDP path. First-ever lap on a combo is not a record, so opening up new content doesn't spam the channel. The message uses the panel language stored server-side (en/es/it) and resolves track/car to their catalogue display names. Webhook URL is treated as a secret β never returned to non-admins / users without the discordWebhook permission.
- Sessions: scrypt password hashing with cost params pinned in
SCRYPT_PARAMS(constant-time compare),HttpOnly; SameSite=Strictcookies with 7-day TTL plus theSecureflag when the request arrived over HTTPS, automatic 401 β logout interceptor on the client. - Forced first-login change: seeded
Admin / Admin1234!is locked into a blocking modal until the password is changed; server-side gate refuses every authenticated endpoint until the flag clears. - CSRF: unsafe methods require
Origin/Refererto matchHost; combined withSameSite=Strictcookies, cross-site requests are rejected at two layers. - Headers: CSP, Permissions-Policy, X-Frame-Options, Referrer-Policy on every response. HSTS auto-enabled when behind an HTTPS-terminating proxy.
- Rate-limited login, change-password, mod uploads, server start/stop/restart and config writes. Per-user concurrent SSE log-stream cap (6) caps file-descriptor usage. Optional
TRUST_PROXY=1to honourCF-Connecting-IP/X-Forwarded-Forso the limiter sees real client IPs through Cloudflare. ADMIN_TOKENheader (when configured) is compared in constant time viacrypto.timingSafeEqualto avoid byte-by-byte timing oracles.- Mod uploads: strict zip-slip abort, archive entry-count and aggregate-size caps, INI value sanitisation against injection.
- Per-user audit log of every admin action, with cursor pagination. Rows are SHA-256 hash-chained with a JSON-canonicalised payload (
chain_version = 1) so a|inside a field cannot collide with a different field assignment; legacy rows still validate.
Sidebar collapses into a drawer with a hamburger toggle below 900 px wide; layouts re-flow to single columns on phones. Tested on portrait phones down to 360 px.
Full interface available in English, Spanish and Italian.
git clone https://github.com/ayozetr/assetto-server-panel.git
cd assetto-server-panel
nvm use 20.20.2
npm install
cp .env.example .env # fill in your server paths
npm start # automatically pre-builds dist/ via esbuild then runs server.jsThe first time you run npm start, esbuild transpiles src/*.jsx to dist/*.js. Subsequent starts re-build (it takes ~20 ms β much cheaper than transpiling in the browser on every page load like before).
To rebuild manually after editing JSX without restarting the server:
npm run buildOpen http://<server-ip>:3000 in your browser.
The default
HOST=0.0.0.0listens on every network interface. Combine that with the wrong network config and the panel becomes reachable from places you didn't intend. Pick one of:
- Cloudflare Tunnel (recommended). Set
HOST=127.0.0.1andTRUST_PROXY=1in.env. The tunnel terminates TLS and forwards traffic to localhost; no firewall ports are opened. See docs/deployment.md.- LAN-only access. Leave
HOST=0.0.0.0but block port 3000 from the internet at the router / firewall. Do not setTRUST_PROXY=1β without a real proxy in front it lets any LAN client spoof their IP inCF-Connecting-IP. (Mitigated by an IP allowlist in 1.5+ but still operator error worth avoiding.)- Reverse proxy (nginx / Caddy / Traefik). Same shape as Cloudflare Tunnel:
HOST=127.0.0.1+TRUST_PROXY=1+TRUST_PROXY_FROM=<proxy IP CIDRs>in.env.If the panel is publicly reachable you must change the default
Adminpassword (the first login forces it, but a port scanner can still see the login screen). Consider also adding Cloudflare Access / Tailscale / a VPN as a second factor.
Default credentials: Admin / Admin1234!. The first login forces a password change in a blocking modal β the rest of the panel is unreachable (server-side enforced) until the password is changed. If you ever lock yourself out, run:
UPDATE panel_users SET must_change_password = 0 WHERE username = 'Admin';in the SQLite DB and log in normally.
| Document | Description |
|---|---|
| AC server setup (SteamCMD) | Install and configure an AC dedicated server from scratch |
| Installation & configuration | Requirements, setup steps and environment variables |
| Docker deployment | Containerised setup with docker compose up -d, volumes, networking, reverse-proxy front-end, troubleshooting |
| Production deployment | Systemd service, Cloudflare Tunnel, firewall, log rotation, hardened systemd unit |
| Authentication & users | Session system, roles and user management |
| Mod installer | Supported formats, auto-detection, chunked upload |
| Database | SQLite schema and what gets stored |
| API reference | All server endpoints |
| Troubleshooting | Common issues and how to fix them |
| Tools | Scripts for extracting and compressing bundled Kunos assets |
| Tested on | Exact OS / Node / npm / SQLite / Python versions the panel is known to run on |
| Security policy | How to report vulnerabilities and what is in / out of scope |
| Roadmap | Project status, comparison with ACSM / Stracker, prioritized backlog |
| Changelog | Per-release summary of every notable change since 1.0.0 |
This is a single-tenant admin tool, not a multi-tenant web app. Trust assumptions:
- Admins are fully trusted. Admin role always passes every permission check. Exclusively allowed actions: managing panel users (create / delete / change role / reset password), editing the AC server
PASSWORDandADMIN_PASSWORDfields, wiping mod history, downloading the SQLite DB and reading or writing the role-permissions set itself. These are reserved by design β exposing them as toggles would let auserescalate to admin. - The
userrole is configurable per-permission, not "read-only". An admin can grant a user the ability to start/stop the AC server, editserver_cfg.ini(everything except the two AC passwords), apply session changes, kick/ban drivers, manage the whitelist, upload mods, edit the Discord webhook, view the audit log and download DB backups β one toggle each. Trust accordingly: grantingserverConfiglets the user reshape the running server; grantingmodUploadlets the user push arbitrary code that runs inside the AC server process (there is no sandbox between mods and the host). - Do not expose the panel to the public internet without HTTPS and credentialled access. Do not give panel accounts to anyone you would not trust with the equivalent shell-level capability. Always set
TRUST_PROXY=1when behind Cloudflare/Tunnel/reverse-proxy so rate limits and audit logs see real client IPs. - The audit log is hash-chained but deletable. Each row stores a SHA-256 of the previous row's hash, so silent edits are detectable with
node tools/verify-audit.jsagainst an external backup. Anyone with shell access toassetto.dbcan still wipe rows entirely β keep periodic backups via/api/admin/backupif you need provable history. Every permission-gated action is recorded with the actor's username, so a per-user trail survives even when a permission set is broad.
What the panel does defend against:
- Anonymous attackers (CSRF, brute-force on login, path traversal, INI injection, decompression bombs, malformed archives).
- Privilege escalation from a compromised user account β even with every toggle on, the user role cannot create/delete panel users, change another user's role or password, read the AC server
PASSWORD/ADMIN_PASSWORD, wipe mod history, or edit the role-permissions set. - Stolen old SW caches (network-first navigation strategy ensures security fixes propagate without manual cache bumps).
What the panel does not defend against:
- Malicious mods (no sandboxing β only grant
modUploadto people you trust to vet upload sources). - A compromised admin account (full control by design).
- An over-permissioned user account β e.g. a user granted
serverConfigcan rewrite ports, ban-list flags and rules; a user grantedmodUploadcan ship arbitrary native code. Defaults exist; the configured permission set is the operator's responsibility. - Filesystem access via the host shell or other services.
- Frontend: React 18 (production CDN) + esbuild build step transpiling JSX β plain JS at startup
- Backend: Node.js native HTTP (no Express)
- Database: SQLite via
better-sqlite3 - Mod extraction:
node-stream-zip,node-unrar-js,node-7z - Real-time logs: Server-Sent Events (SSE)
The combinations below are the ones the maintainer actually runs day-to-day; the panel is reasonably portable but these are the ones that are known to work. Full version matrix and bundled-dependency lockfile excerpt in docs/tested-on.md.
| Role | OS | Kernel | Node.js | npm | SQLite (system) | Python |
|---|---|---|---|---|---|---|
| Production | Ubuntu 24.04.4 LTS (Noble Numbat) | 6.8.0-111-generic | v20.20.2 | 11.13.0 | 3.45.1 | 3.12.3 |
| Development | CachyOS (Arch-based, rolling) | 7.0.5-2-cachyos | v20.20.2 | 11.14.1 | 3.53.0 | 3.14.4 |
Bundled npm packages on both hosts (production identical, dev installs the same lockfile):
7zip-bin@5.2.0 Β· better-sqlite3@12.10.0 Β· dotenv@16.6.1 Β· node-7z@3.0.0 Β· node-stream-zip@1.15.0 Β· node-unrar-js@2.0.2 Β· esbuild@0.28.0
Other Linux distributions on Node 20 LTS should work without changes β the panel only depends on a POSIX filesystem, a recent SQLite, and the libraries above. Windows/macOS will likely run but are not exercised regularly; please open an issue if you hit something distro-specific.
Source-available. The full text β which is what binds you, not this summary β is in LICENSE. Read it before deploying.
Short version (informative, not legally operative β only the LICENSE file is):
-
Free to download and run anywhere, for anything lawful. Personal hobby use, private leagues, friend groups, amateur clubs, educational and research use are all fine. So is operating the panel for public servers, including servers advertised on the Kunos public-server list / Content Manager lobbies, including commercial use (paid leagues, sponsorships, ads, donations, for-profit organizations). You don't need a separate agreement to charge for participation or run a paid event on top of a server the panel manages.
-
No redistribution. You may not republish the source code, fork it to a public repository as your own, upload it to a package registry, bundle it into another product, host it as a service for third parties to download, or otherwise hand copies to other people. If someone wants to use the panel, point them at the official repository β they can clone it themselves under their own acceptance of the LICENSE.
-
Attribution Marks are irremovable. The "Developed by ayozetr" credit in the sidebar, the project name "Assetto Server Panel", the link to the official repository, the copyright notice, and every other identifier referring to the author or the project must stay intact in every copy you operate. This applies even to commercial deployments. A re-skin, white-label, or theme that hides the marks is a breach and terminates your rights automatically.
-
Local modifications are allowed. Patch bugs, translate, add integrations, restyle (without touching the Attribution Marks) β keep them for yourself. You may submit them upstream via pull request; you may not publish them as your own fork.
-
No affiliation with anyone. Not Kunos Simulazioni, not Valve, not 505 Games, not the Content Manager / acstuff projects, not any car / track manufacturer whose brand appears in bundled assets. The LICENSE has a long disclaimer section spelling this out.
-
No warranty, no liability. "AS IS". Each tagged release is published with good-faith effort against known vulnerabilities at the time of publication, but the author makes no ongoing warranty and accepts no liability for damages, data loss, or downtime. See LICENSE sections 7 and 8.
For licensing questions, redistribution requests, or anything else: ayozetr@proton.me. For security disclosure see SECURITY.md.







