Version: 26.5.0 Updated: 2026-06-11 Status: Active
Rackula is a lightweight, FOSS, web-based rack layout designer for homelabbers to plan optimal equipment arrangement before physical mounting.
"I have a rack and a pile of gear — I need to figure out the optimal arrangement before I start mounting."
Homelabbers planning rack layouts. Desktop browser users for creation/editing, mobile for viewing.
- ADHD-friendly — Minimal decision points, visual clarity
- Lightweight — Runs entirely in the browser; optional backend for persistence
- Portable — Layouts saved as self-contained
.Rackula.ziparchives - Multi-rack — Support multiple racks per layout
- Single-level nesting — Containers hold devices but cannot be nested
- FOSS — MIT licensed
| Resource | URL |
|---|---|
| Production | https://count.racku.la/ |
| Dev | https://d.racku.la/ |
| Repository | https://github.com/RackulaLives/Rackula |
Rackula supports two deployment modes:
| Mode | Backend | Persistence | Use Case |
|---|---|---|---|
| Static | None | File download/upload | Cloudflare Workers, static hosts |
| Persist | Hono/Bun | API server (save/load) | Self-hosted Docker |
In static mode, Rackula is a pure client-side SPA with no network dependencies. In persist mode, an optional API server provides layout storage, authentication, and multi-device sync.
See CLAUDE.md for deployment environments and CI/CD details.
| Component | Technology |
|---|---|
| Framework | Svelte 5 (runes: $state, $derived, $effect) |
| Language | TypeScript (strict mode) |
| Rendering | SVG |
| Pan/Zoom | panzoom |
| Persistence | File download/upload (.Rackula.zip) |
| API Server | Hono on Bun (optional, for persist mode) |
| Data Format | YAML (js-yaml) |
| Validation | Zod |
| Styling | CSS custom properties (design tokens) |
| Testing | Vitest + @testing-library/svelte + Playwright |
| Build | Vite |
Reference: For complete schema documentation including all fields, validation rules, and YAML examples, see SCHEMA.md.
Rackula models rack mounting carrier-first. This is a design principle: it decides where a device can sit and answers the recurring "why can't I place this at U5 1/3?" question with one authoritative rule.
Rails register equipment at whole-U boundaries only, matching the EIA-310 standard. A U is defined by three mounting holes; those holes locate the boundary of a unit, they are not alternative mounting positions. There is no real 1/3U or 1/2U rail position on a physical rack, so Rackula does not offer one. Rail positions are whole-U integers.
Devices smaller than a full U, or narrower than full width, do not bolt to the rails on their own. They mount inside a carrier: a bracket, tray, or shelf that occupies a whole U. The carrier registers to the rails at a whole-U boundary, and the small devices register to the carrier. A half-width pair, for example, is one carrier with two cells side by side. A MikroTik K-79 is a branded 2x2 carrier holding up to four small units in 1U.
The mounting rule:
| Device | Mounts on |
|---|---|
| Full-width, integer u_height | Rails |
| Sub-U height, non-integer height, or sub-width | A carrier |
Carriers are ordinary devices with a face and a whole-U position. A device with u_height below 1, a non-integer u_height, or less than full width cannot be placed on the rails directly; it must be a child of a carrier. This rule is enforced in the schema, in store placement actions, and in drag-and-drop targeting, so a fractional rail position cannot be reintroduced from any path.
A device is less than full width when it has slot_width: 1 (half width) or a measured width_mm. Inside a carrier, a cell accepts a measured device when width_mm is no more than the cell's width_fraction of the rack's clear opening, plus 0.5 mm. Cells of 0.33 and 0.66 count as the exact thirds they round down from. The clear opening is the published one where it exists: 222.25 mm (8.75 in) for 10", 450 mm for 19" (EIA-310 minimum), and 500 mm for 21" (ETSI ETS 300 119-3). 23" has no published opening and is approximated as the nominal width minus 1.25 in. Without width_mm, a half-width device needs a cell of at least 0.5 and a full-width device a cell of 1.0.
A measured device may also carry height_mm, from which its u_height is derived: the smallest multiple of 0.5U that holds it, within 0.5 mm. In a carrier it is drawn at height_mm, on the floor of its cell, so its image keeps the device's proportions. A measured device can be turned 90 degrees onto its side, and back, before or after it is placed; 0 and 90 are the only angles. Turned, its width and height swap, so every cell fit uses the device as it stands. Its generated carrier is reshaped with it: the cell is recut to the turned width and the carrier takes the whole U that holds its tallest child. A turn that does not fit is refused.
Because a measured device's fit depends on the rack, a rack width change or a move to another rack recuts each generated carrier so its cells keep their devices' measured widths. The change is refused when a measured child would no longer fit a shipped carrier's cell, or a generated carrier's cells and gaps would no longer fit the new opening. A measured device gets a carrier generated around one cell cut to its own size, whatever its height: the carrier takes the whole U that holds the cell, so only a width past the rack's clear opening leaves it placeable only in an existing shelf or carrier with a wide enough cell.
Rationale: fractional rail offsets modelled a physical fiction and scattered paired half-width gear into impossible positions. Carrier-first keeps the data faithful to how equipment actually mounts, and gives sub-U devices a real parent rather than a floating coordinate.
Legacy layouts and share links that used fractional rail positions are adapted on load: positions snap to the nearest whole U, and sub-U or paired half-width placements are wrapped in an automatically synthesized carrier. The adaptation runs once on the read path. Older files open with their gear intact, now mounted in carriers the user did not place by hand.
| Constraint | Value |
|---|---|
| Min device height | 0.5U |
| Max device height | 42U |
| Min rack height | 1U |
| Max rack height | 100U |
| Allowed rack widths | 10", 19" |
| Max racks per layout | No limit |
| Max container nesting | 1 level |
| Max image size | 5MB |
| Supported image formats | PNG, JPEG, WebP |
Device types can define interfaces[] port templates (see SCHEMA.md for the InterfaceTemplate shape). Interface data is optional; most starter and brand-pack devices are visual-only today. Brand packs (src/lib/data/brandPacks/) do not yet carry interface data; that population is tracked as deferred follow-up work (#260).
Starter library devices (src/lib/data/starterLibrary.ts) that ship with interface data:
| Device | Slug | Interfaces |
|---|---|---|
| Switch (24-Port) | 24-port-switch |
24x 1000base-t |
| Switch (48-Port) | 48-port-switch |
48x 1000base-t |
| Half Switch (8-Port) | 1u-half-switch |
8x 1000base-t |
| Fiber Patch Panel | 1u-fiber-patch-panel |
12x 1000base-x-sfp |
| Patch Panel (24-Port) | 24-port-patch-panel |
24x 1000base-t |
| Patch Panel (48-Port) | 48-port-patch-panel |
48x 1000base-t |
| Half Patch Panel | 1u-half-patch-panel |
12x 1000base-t |
| Amplifier (1U) | 1u-amplifier |
xlr-3 inputs, speakon outputs, rs-232 control |
| Amplifier (2U) | 2u-amplifier |
xlr-3 inputs, speakon outputs, rs-232 and management control |
| Audio Processor | 1u-audio-processor |
xlr-3 in, trs-1-4 out, aes3 out, dual dante, rs-232 control |
| AV Receiver (1U) | 1u-av-receiver |
hdmi in/out, rca aux in, rs-232 control |
| AV Receiver (2U) | 2u-av-receiver |
hdmi in/out, rca aux in, management and rs-232 control |
| Power Amplifier (3U) | 3u-power-amplifier |
xlr-3 inputs, speakon outputs, dual dante, management control |
| Streaming Encoder | 1u-streaming-encoder |
hdmi and sdi-bnc in, xlr-3 audio in, 1000base-t network |
| Video Switcher | 1u-video-switcher |
hdmi/displayport/vga in, hdmi out, rs-232 and management control |
Two rail-level devices collide if both conditions are true:
- Their U ranges overlap (
positiontoposition + u_height - 1) - Their faces collide (see rules below)
Rail-level devices are all whole-U and full-width (see Mounting Model above), so there is no left/right slot dimension to compare at the rails. Sub-U gear lives inside carriers, where collision is checked per cell (see Container-Aware Collision below).
Face Collision Rules (Face-Authoritative Model):
The face property is the single source of truth for collision detection:
| Face A | Face B | Collision? |
|---|---|---|
| both | any | YES |
| front | front | YES |
| rear | rear | YES |
| front | rear | NO |
Key Principle: If a device's face is explicitly set to front or rear, it only blocks that face — regardless of the device type's is_full_depth property.
Role of is_full_depth:
- Determines the DEFAULT face when placing a device
- Full-depth devices (
is_full_depth: trueor not specified) default toface: "both" - Half-depth devices (
is_full_depth: false) default toface: "front" - Users can override face via EditPanel; the override takes precedence for collision detection
Interface Position Constraints:
Half-depth device types (is_full_depth: false) have a single physical face. Their interface templates cannot have mixed position values — all explicitly positioned interfaces must use the same face (front or rear). Unpositioned interfaces default to front. This constraint is enforced by DeviceTypeSchema.
Interaction Consistency:
Both drag-and-drop and keyboard movement use face-aware validation:
| Operation | Validation Parameters |
|---|---|
| Drag-and-drop | Target face from faceFilter prop |
| Keyboard (↑/↓) | Face from placedDevice.face |
Collision detection is hierarchical:
- Rack-level: Container devices collide with other rack-level devices using normal rules. Child devices (those with
container_id) are invisible to rack-level collision. - Container-level: Children only collide with siblings in the same container. Child position is relative (0 = bottom of container). Children must fit within the container's
u_height.
| Device A | Device B | Collision Check |
|---|---|---|
| Rack-level | Rack-level | Normal U/face/slot rules |
| Rack-level | Child | Never collide |
| Child (container X) | Child (container X) | Check within container |
| Child (container X) | Child (container Y) | Never collide |
Children inherit their parent container's face for rendering purposes.
Extension: .Rackula.zip
my-rack.Rackula.zip
└── my-rack/
├── my-rack.yaml # Layout data
└── assets/
├── device-types/ # Device type default images
│ └── [device-slug]/
│ ├── front.webp
│ └── rear.webp
└── placements/ # Placement override images (optional)
└── [placement-id]/
├── front.webp
└── rear.webp
device-types/— Images uploaded when creating device types (shared by all instances)placements/— Per-placement image overrides (keyed byPlacedDevice.id)- Bundled images are not stored in archives (loaded from app assets)
| Document | Purpose |
|---|---|
| ARCHITECTURE.md | Codebase overview and entry points |
| SCHEMA.md | Complete data schema reference |
| BRAND.md | Design system and brand colours |
| CLAUDE.md | Development workflow and commands |
| CHANGELOG.md | Version history |