Scratching Post — Sunbeam Digital Signage
A self-hosted digital notice board / signage system built on ASP.NET Core 8, designed for IIS on Windows Server 2019/2022.
Terminology
| Component | Name | Description |
|---|---|---|
| Individual slide | Meow | A single content slide — WYSIWYG, embed, or calendar |
| Slide collection | PURR | Persistent User Requests and Reminders — a device playlist |
| Priority slide | ASK | Attention Seeking Kitty — overrides collection order (planned) |
| Display node | Kitten | A device that plays a playlist of Meows |
| Backend | Scratching Post | The admin panel where all content is managed |
| Framework | Sunbeam | The underlying system powering everything |
Features
- WYSIWYG Editor — TinyMCE with image upload, tables, rich text
- Embed Web Content — iframe any URL as a slide
- ICS Calendar — display upcoming events from any .ics feed
- Device Playlists — assign slides to specific devices with individual durations
- Per-Device Resolution — configure display resolution per device
- URL-Based Devices — access displays via
http://server/frontofhouse,http://server/staffroom - Drag-and-Drop Playlist — reorder slides in the admin
- Live Updates — display polls for changes every 30 seconds
- Full-Screen Display — hidden cursor, clock overlay, progress bar
- Smooth Transitions — fade or slide transitions
- Meow Expiration — give any slide an expiry date/time; it's automatically skipped on displays afterwards (and stays in the library, unedited, in case you need it again)
- Docker Support — run via
docker compose upas an alternative to IIS, with volumes for the database and uploads
Requirements
IIS (Windows Server):
- Windows Server 2019 or 2022
- IIS with ASP.NET Core Hosting Bundle
- .NET 8 SDK (for building) / .NET 8 Runtime (for hosting)
Docker (any host):
- Docker Engine + Docker Compose — see Docker Deployment below
Quick Start
1. Install Prerequisites
Download and install:
- .NET 8 SDK
- ASP.NET Core Hosting Bundle (under Hosting Bundle)
2. Build
cd ScratchingPost
dotnet restore
dotnet publish -c Release -o ./publish
3. Deploy to IIS
- Open IIS Manager
- Create a new Site (e.g. ScratchingPost)
- Set the physical path to the
publishfolder - Set the binding (e.g. port 80, or a hostname like
sign.local) - Set the Application Pool to No Managed Code (the Hosting Bundle handles .NET)
- Ensure the App Pool identity has write access to the publish folder (for SQLite DB and uploads)
4. First Run
The SQLite database (noticeboard.db) is created automatically on first start with two demo devices seeded:
- Front of House at
http://server/frontofhouse - Staff Room at
http://server/staffroom
5. Access
| URL | Purpose |
|---|---|
http://server/admin |
Scratching Post admin panel |
http://server/admin/slides |
Manage Meows (slides) |
http://server/admin/devices |
Manage Kittens (devices) |
http://server/frontofhouse |
Front of House display |
http://server/staffroom |
Staff Room display |
Docker Deployment
An alternative to IIS — runs anywhere Docker does (Linux, Windows with WSL2 backend, Synology/QNAP NAS, a Raspberry Pi, etc.).
1. Build and run
git clone https://gitea.hideawaygaming.com.au/jessikitty/ScratchingPost.git
cd ScratchingPost
docker compose up -d --build
That's it — the container listens on port 8080, so open http://server:8080.
2. What the compose file sets up
docker-compose.yml builds the image from the included Dockerfile and wires up two bind-mounted folders next to the compose file so your data survives container rebuilds/updates:
| Host path | Container path | Purpose |
|---|---|---|
./data |
/data |
noticeboard.db (SQLite) |
./uploads |
/app/wwwroot/uploads |
Images uploaded via TinyMCE, plus uploaded ICS files |
Both folders are created automatically on first run.
3. Configuration
Same settings as IIS, just as environment variables in docker-compose.yml (nested keys use a double underscore):
environment:
- DataDirectory=/data
- TimeZone=UTC
- Admin__Username=admin
- Admin__Password=ScratchingPost2026!
- MaxUploadSizeMB=20
Change the admin password before exposing this beyond your local network.
4. Updating
git pull
docker compose up -d --build
The database and uploads persist across rebuilds since they live in the bind-mounted ./data and ./uploads folders, not inside the image. On startup the app checks the SQLite schema and adds any new columns it needs (e.g. the ExpiresAt column below) automatically — no manual migration step.
5. Reverse proxy / HTTPS
The container serves plain HTTP on 8080. For a public-facing deployment, put it behind a reverse proxy (Caddy, nginx, Traefik) that terminates HTTPS and forwards to scratchingpost:8080.
Running IIS and Docker side by side
The IIS deployment and the Docker image are built from the same codebase and don't share a database or uploads folder by default — they're independent instances unless you point them at the same SQLite file and uploads directory yourself.
Configuration
Edit appsettings.json:
{
"MaxUploadSizeMB": 20
}
How It Works
- Create Meows (slides) — use the WYSIWYG editor for rich content, embed external URLs, or point to an ICS calendar feed
- Create Kittens (devices) — give each a name, URL slug, and display resolution
- Build PURRs (playlists) — assign Meows to Kittens with individual durations and drag-and-drop ordering
- Display — point a browser at
http://server/{slug}and it runs full-screen with smooth transitions
The display page polls for changes every 30 seconds, so updates appear on devices without restarting.
Meow Expiration
Any Meow can be given an Expires date/time in its editor (under Scheduling). Once that time passes:
- The Meow is automatically skipped by every Kitten it's assigned to — no manual removal needed
- It stays fully intact in Meows (with a red Expired badge) so it can be re-used or have its expiry pushed back later
- Nothing is deleted — expiry only affects playback, not the stored slide
Leave it blank for a Meow that should run indefinitely.
Time zone: expiry is checked against the TimeZone setting, not the raw OS clock:
- IIS —
appsettings.jsonships with"TimeZone": "", which falls back to the Windows Server's own local time zone (the original behaviour — no change needed on upgrade). - Docker —
docker-compose.ymlsetsTimeZone=UTCby default, since containers are otherwise timezone-ambiguous. Override it to an IANA id (e.g.Australia/Melbourne) if you'd rather enter expiry times in local time than UTC; the image includestzdataso real zone names resolve correctly.
Display Controls
Keyboard shortcuts on display pages (useful during setup):
| Key | Action |
|---|---|
| Right Arrow or Space | Skip to next slide |
| Left Arrow | Go to previous slide |
| F or F11 | Toggle fullscreen |
| C | Toggle cursor visibility |
Data Storage
- Database: SQLite (
noticeboard.db) — created automatically, zero config - Uploads:
wwwroot/uploads/— images uploaded via TinyMCE or the file upload API - On IIS, both need write permissions for the App Pool identity
- On Docker, both live in bind-mounted host folders (
./data,./uploads) — see Docker Deployment
TinyMCE Note
The WYSIWYG editor uses TinyMCE CDN with no-api-key. This shows a small notification banner. For production, register for a free TinyMCE API key at tiny.cloud and replace no-api-key in Views/Slides/Create.cshtml and Edit.cshtml.
Project Structure
ScratchingPost/
├── Controllers/ # MVC controllers
│ ├── AdminController # Dashboard
│ ├── SlidesController # Meow CRUD
│ ├── DevicesController # Kitten CRUD + Playlist management
│ ├── DisplayController # Device display + playlist API
│ └── ApiController # Image upload, ICS parsing
├── Data/ # EF Core DbContext + seed data
├── Infrastructure/ # Clock (time-zone-aware "now" for Meow expiry)
├── Models/ # Slide, Device, DeviceSlide
├── Routing/ # DeviceSlugConstraint
├── Views/ # Razor views (Admin, Slides, Devices, Display)
├── wwwroot/ # Static files
│ ├── css/ # admin.css, display.css
│ ├── js/ # admin.js, display.js (Sunbeam engine)
│ └── uploads/ # User-uploaded images
├── appsettings.json # Configuration
├── web.config # IIS configuration
├── Dockerfile # Container build (alternative to IIS)
├── docker-compose.yml # Container run config (ports, volumes)
├── .dockerignore
└── NoticeBoard.csproj # Project file
Version History
- v1.1.1 — Meow expiry now checked against a configurable
TimeZonesetting instead of the raw OS clock: IIS keeps its existing local-time behaviour by default, Docker defaults to UTC (overridable to any IANA zone; the image ships withtzdata) - v1.1.0 — Docker deployment (
Dockerfile+docker-compose.yml, configurable data directory, volume-backed DB/uploads), Meow expiration (ExpiresAtfield, automatic playback filtering, auto-migrating column for existing databases) - v1.0.0 — Initial release: WYSIWYG slides, embed, ICS calendar, device playlists, drag-and-drop ordering, live polling, IIS-ready deployment