← All assets

Community Platform

Ruins Empire

A community that runs like an organisation and broadcasts like a network: ranks, vetting, shifts and casework on one side, a 24/7 radio station and scheduled TV on the other.

Live · ruinsempire.com
23
Permission flags

scoped by realm and domain

27
Data models
151
API route files

across 30 areas

43,556
Lines of code

across 43 pages

What it is

Inside Ruins Empire

Ruins Empire is a gaming community, and the software behind it is two systems sharing one permission kernel. On one side is the organisational half: people apply, are vetted, are accepted or rejected with a written reason, are placed on a rank ladder, join a department, claim shifts, and — when something goes wrong between two members — end up in a case file. On the other is a broadcast network: a 24/7 radio station, scheduled television with its own programming grid, and synced watch parties. One Nuxt application serves the public site, the staff back-office and both broadcast chains.

The organisational half is the clearest answer we have to “what can you actually build?”, because almost none of it is game-specific. Strip the insignia and it is a membership platform: twenty-three permission flags with realm and domain scoping, an application pipeline that provisions and de-provisions real access, per-division rank ladders with rosters, a shift scheduler with recurrence and self-claim, a form builder, and confidential casework with per-case access control.

The broadcast half is the part people actually use daily, and it is built on one idea reused twice. A station is a playlist, a cursor, and the timestamp that cursor started at — nothing more. There is no scheduler process and no per-listener state; what is playing right now is computed from the wall clock on demand, so everyone who tunes in agrees. Television channels are the same object with longer items, plus a programming grid on top: shows with seasons and episodes, recurring weekly slots, priorities and validity windows, and an auto-DJ that fills the gaps. Either one can be taken over live — radio through Liquidsoap into Icecast, television through RTMP into MediaMTX — by flipping a single flag.

What is worth looking at closely is what happens when things fail. An approval that cannot actually grant the applicant their access is refused outright and nothing is recorded, because an approval that provisioned nothing has achieved nothing. A rejection that cannot revoke access still records the decision and warns the reviewer — the opposite trade, and the right one, because otherwise nobody who had already left could ever be formally rejected. The current work in the repo is in the same spirit: gapless playback across track boundaries, and a watchdog that notices a stalled stream and recovers it.

Built with

  • Nuxt 4
  • Vue 3
  • MongoDB + Mongoose
  • better-auth
  • Socket.IO
  • Tailwind 4
  • Discord Embedded App SDK
  • Liquidsoap + Icecast
  • MediaMTX (RTMP)
  • HLS.js
  • ffmpeg

Capabilities

What it actually does.

01

Twenty-three permission flags, scoped two ways

A bitfield evaluated from one shared module on both client and server, with roles scoped to a realm and a domain. A domain-scoped role can never edit realm-level content, but may edit things that have no domain to scope against — two deliberately different checks, with a comment warning the next reader not to merge them. Six of the flags govern the broadcast side alone. The admin role re-asserts every flag on read, so a newly added permission is never silently missing from it.

02

Applications that provision real access

A built questionnaire, a review queue, and a decision that reaches out and grants or revokes the applicant’s actual access. Approval refuses to record itself if the grant fails; rejection records itself even if the revoke fails, and says so. Each answer stores the question’s wording alongside it, so editing the form later cannot rewrite an application already on file.

03

Rank ladders and rosters

Per-division ladders with tiers, requirements and insignia, and a roster that joins people to ranks. Uniqueness is per person per division rather than per person, so someone can hold an independent standing in each. Tier labels are copied onto each rank on purpose: a half-applied rename still renders, which is a far better failure mode than a dangling reference.

04

A shift scheduler with one recurrence engine

Members publish availability; managers schedule against it. Shifts carry a slot count, so an under-filled shift is an open shift anyone can claim — and a null assigner is how a self-claim is distinguished from an assignment. One expansion engine serves both the simple weekly rules and the full daily/weekly/monthly/yearly rules, capped so that “every day for fifty years” cannot take the server down.

05

Confidential casework

Complaints and two-party dispute files, each with a status workflow, an assignee, an append-only log of notes and communications with author attribution, and a per-case access list so a file is visible only to the people who need it. It is the one part of the system that keeps a real history rather than just current state.

06

A form builder whose answers cannot be corrupted

Fields are embedded documents whose ids survive a whole-array rewrite, and array position is the ordering — so reordering is one write and a client can never reach another form’s fields. Every answer denormalises the field’s label and its type, so renaming, retyping or deleting a field never rewrites data already collected.

07

A radio station with no broadcaster

A station is a playlist, a cursor, and the time that cursor started. Nothing schedules it. When anyone asks what is playing, the server walks forward from the stored timestamp, popping tracks whose duration has already elapsed — and crucially it adds each track’s length to the previous start rather than to the current time, so position stays exact no matter how irregularly anyone polls. A loop bound stops a playlist of zero-length tracks spinning, and a five-minute guard resets rather than fast-forwarding through hours of silence.

08

The library behind the station

Uploads carry title, artist, album, genre, duration and cover art, with a per-track flag deciding whether listeners may download it as well as stream it. Stations are assembled from that library with optional shuffle, and the now-playing feed drives the on-site player, the floating mini-player, and the metadata shown on Discord profiles.

09

Television, scheduled like television

A channel is the same object as a station with longer items. On top sits a real programming grid: shows with seasons and episodes that keep their own playhead, schedule entries with weekday and time-of-day slots, weekly recurrence, priorities and validity windows, and a record of when each last aired. An auto-DJ fills whatever the grid leaves empty, so the channel is never dead air.

10

Live takeover, on both chains

Either a station or a channel can be handed to a live source by flipping one flag. Radio goes out through Liquidsoap into Icecast; television through RTMP into MediaMTX, fronted by its own nginx. The scheduled playout is what runs when nobody has taken the microphone, rather than a fallback bolted on afterwards.

11

Synced watch parties

A host opens a room and shares a code that is valid only inside a time window. Everyone holding it watches in lockstep — play, pause and seek propagate, and a live viewer count comes back. Room state is held in process, which is precisely why the app is pinned to a single worker: cluster mode gave each worker its own room table and silently broke both the sync and the counter.

12

Uploads that get out of the way

Large video arrives in chunks and is finalised server-side, then handed off to the media service rather than being transcoded in the request path. Subtitles can be attached afterwards, and playback falls back to direct streaming if the transcode has not finished or the service cannot be reached.

13

The site runs inside Discord

It boots as a Discord Activity, which breaks cookie authentication outright — a cookie set in that iframe belongs to Discord’s proxy host, not to us. So the Activity authenticates with a token and mints a single-use handoff link at the moment it is clicked, landing the member on the real site already signed in without typing anything. The radio plays inside Discord too, and sets the listener’s Rich Presence while it does.

Related

Where it sits.

Want something like this?

We build the same way for other people.