DIY server in PHP/Laravel for self hosted screen generation.
  • PHP 66.2%
  • Blade 30.4%
  • HTML 1.4%
  • JavaScript 0.9%
  • Liquid 0.7%
  • Other 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-08 20:58:36 +01:00
.devcontainer chore: make the Vite dev server reachable from the Dev Container 2026-10-07 20:33:55 +02:00
.github chore: update gh actions to node 24 2026-08-28 21:15:16 +02:00
app feat: bump laravel-liquid; add compabiliy 2026-11-08 19:47:23 +01:00
bootstrap feat: add inital MCP server as Lab feature 2026-08-02 21:12:34 +02:00
config feat: implement Blade security hardening 2026-11-06 17:53:33 +01:00
database feat: add oss catalog advisory warning 2026-11-06 18:14:24 +01:00
docker/prod Comment out FORCE_HTTPS in docker-compose.yml 2026-09-25 18:10:56 +02:00
docs chore: update dependencies 2026-11-08 20:58:36 +01:00
lang/de
public feat: add Simulate physical device toggle to mirror WebUI 2026-11-01 14:33:57 +01:00
resources feat: add oss catalog advisory warning 2026-11-06 18:14:24 +01:00
routes fix: harden device resolution and screen update endpoints; add rate limiting 2026-11-06 17:53:32 +01:00
screenshots
storage
tests feat: add oss catalog advisory warning 2026-11-06 18:14:24 +01:00
.dockerignore
.editorconfig
.env.example feat: implement Blade security hardening 2026-11-06 17:53:33 +01:00
.env.example.local feat: implement Blade security hardening 2026-11-06 17:53:33 +01:00
.gitattributes
.gitignore chore: update js dependencies 2026-10-22 17:04:05 +02:00
.npmrc
artisan
composer.json feat: bump laravel-liquid; add compabiliy 2026-11-08 19:47:23 +01:00
composer.lock chore: update dependencies 2026-11-08 20:58:36 +01:00
CONTRIBUTING.md
docker-compose.yml
Dockerfile fix(#320): bump to node v24.22.1 2026-11-05 23:04:05 +01:00
example.png
LICENSE.md
package-lock.json chore: update dependencies 2026-11-08 20:58:36 +01:00
package.json chore: bump puppeteer 2026-11-05 22:54:27 +01:00
phpstan.neon
phpunit.xml feat: implement Blade security hardening 2026-11-06 17:53:33 +01:00
pint.json chore: pint —blade 2026-08-14 23:04:40 +02:00
README.md feat: implement Blade security hardening 2026-11-06 17:53:33 +01:00
README_byos-devices.jpeg
README_byos-screenshot-dark.png
README_byos-screenshot.png
rector.php chore: rector, pint 2026-08-14 22:47:53 +02:00
vite.config.js Revert "chore: make the Vite dev server reachable from the Dev Container" 2026-10-07 23:06:06 +02:00

LaraPaper (PHP/Laravel)

tests

usetrmnl%2Flarapaper | Trendshift

LaraPaper is a self-hostable implementation of a TRMNL server (BYOS), built with Laravel. It allows you to manage TRMNL-compatible devices, generate screens using native plugins (Screens API, Screenshot, Image Webhook), recipes (170+ from the OSS community catalog, 1000+ from the TRMNL catalog, or your own), or the API, and can also act as a proxy for the native cloud service (Core). With over 400k downloads and 400+ stars, it’s the most popular community-driven BYOS.

Screenshot Screenshot

Key Features

  • 📡 Device Information – Display battery status, WiFi strength, firmware version, and more.
  • 🔍 Auto-Join – Automatically detects and adds devices from your local network.
  • 🖥️ Screen Generation – Supports Plugins (including Mashups), Recipes, API, Screenshot, Image Webhook, Markup, or updates via Code.
  • 🔄 TRMNL API Proxy – Can act as a proxy for the native cloud service (requires TRMNL Developer Edition).
    • This enables a hybrid setup – for example, you can update your custom Train Monitor every 5 minutes in the morning, while displaying native TRMNL plugins throughout the day.
  • 🌙 Dark Mode – Switch between light and dark mode.
  • 🐳 Deployment – Dockerized setup for easier hosting (Dockerfile, docker-compose).
  • 💾 Flexible Database configuration – uses SQLite by default, also compatible with MySQL or PostgreSQL
  • 🛠️ Devcontainer support for easier development.

Devices

Support ❤️

This repo is maintained voluntarily by @bnussbau.

Support the development of this package by purchasing a TRMNL device through the referral link: https://trmnl.com/?ref=laravel-trmnl. At checkout, use the code laravel-trmnl to receive a $15 discount on your purchase.

or

"Buy Me A Coffee"

GitHub Sponsors

Hosting

Run everywhere, where Docker is supported: Raspberry Pi, VPS, NAS, Container Cloud Service (Cloud Run, ...). For production use, generate a new APP_KEY (php artisan key:generate --show) and set the environment variable APP_KEY=. For personal use, you can disable registration (see section Environment Variables).

Docker Compose

Docker Compose file located at: docker/prod/docker-compose.yml.

Backup Database
docker ps #find container id of larapaper container
docker cp {{CONTAINER_ID}}:/var/www/html/database/storage/database.sqlite database_backup.sqlite
Updating via Docker Compose
docker compose pull
docker compose down
docker compose up -d

VPS

If you’re using a VPS (e.g., Hetzner) and prefer an alternative to native Docker, you can install Dokploy and deploy LaraPaper using the integrated Template. It’s a quick way to get started without having to manually manage Docker setup.

PikaPods

You can vote for LaraPaper to be included as PikaPods Template here: feedback.pikapods.com

Umbrel

Umbrel is supported through a community store, see.

Other Hosting Options

Laravel Forge, or bare metal PHP server with Nginx or Apache is also supported.

Requirements

Local Development

see docs/DEVELOPMENT.md

Demo Plugins

Run the ExampleRecipesSeeder to seed the database with example plugins:

php artisan db:seed --class=ExampleRecipesSeeder
  • Zen Quotes
  • This Day in History
  • Weather
  • Train Departure Monitor
  • Home Assistant
  • Sunrise/Sunset

Usage

Environment Variables

Environment Variable Description Default
TRMNL_PROXY_BASE_URL Base URL of the native TRMNL service https://trmnl.app
TRMNL_PROXY_REFRESH_MINUTES How often should the server fetch new images from native service 15
TRMNL_BLADE_FRAMEWORK_BASE_URL Base URL for versioned TRMNL framework CSS/JS used when rendering recipes https://trmnl.com
MULTIUSER_REGISTRATION_ENABLED After the first account, allow more signups and disable user-authored Blade. See the security note below. 0
INSECURE_BLADE_ALLOW_PHP_EXECUTION Force compiling user Blade as PHP on (1) or off (0). Unset: on for a single user, off for multi-user. 1 in multi-user mode is remote code execution for every registered user. auto
PASSKEYS_ENABLED Enable Passkeys for login (requires HTTPS) 0
SSL_MODE SSL Mode, if not using a Reverse Proxy (docs) off
FORCE_HTTPS If your server handles SSL termination, enforce HTTPS. Alternative: TRUSTED_PROXIES. 0
TRUSTED_PROXIES If your server handles SSL termination, allow mixed mode. e.g. "172.0.0.0/8" or * null
PHP_OPCACHE_ENABLE Enable PHP Opcache 0
TRMNL_IMAGE_URL_TIMEOUT How long TRMNL waits for a response on the display endpoint. (sec) 30
HTTP_CLIENT_TIMEOUT Outbound HTTP timeout when fetching recipe polling URLs. (sec) 10
APP_TIMEZONE Default timezone, which will be used by the PHP date functions. UTC is recommended. UTC

Login

If your environment is local, you can access the server at http://localhost:4567 and login with user / password admin@example.com / admin@example.com, otherwise register the first account. Further signup stays closed unless MULTIUSER_REGISTRATION_ENABLED=1.

Security: Turning on multi-user registration disables user-authored Blade unless INSECURE_BLADE_ALLOW_PHP_EXECUTION=1. That insecure flag lets any registered user execute PHP on the server, potentially accessing other users' data. Set INSECURE_BLADE_ALLOW_PHP_EXECUTION=0 to disable Blade on a single-user server too.

➕ Add Your TRMNL Device

Auto-Join (Local Network)
  1. Switch on the “Permit Auto-Join” toggle in the header. For that to work only one user can be registered.
  2. New devices on your local network will be detected and added automatically when connecting to the server.

✅ This is the easiest way to connect your devices with minimal effort.

Manually
  1. Open the Devices page: 👉 http://localhost:4567/devices
  2. Click “Add New Device”.
  3. Retrieve your TRMNL MAC Address and API Key:
  • You can grab the TRMNL Mac Address and API Key from the TRMNL Dashboard
  • Alternatively, debug incoming requests to /api/setup to determine them

⚙️ Configure Server for Device

📌 Firmware Version 1.4.6 or Newer

  • Setup device
  • After entering Wifi credentials, choose "Custom Server"
  • Point to the URL of your server

Firmware Older Than 1.4.6

If your device firmware is older than 1.4.6, you need to flash a new firmware version to point it to your server.

See this YouTube guide: https://www.youtube.com/watch?v=3xehPW-PCOM

☁️ Activate fresh TRMNL Device with Cloud Proxy

  1. Setup the TRMNL as in the official docs with the cloud service (connect one of the plugins to later verify it works)
  2. Setup LaraPaper, create a user and login
  3. In LaraPaper in the header bar, activate the toggle "Permit Auto-Join"
  4. Press and hold the button on the back of your TRMNL for 5 seconds to reactivate the captive portal (or reflash).
  5. Go through the setup process again, in the screen where you provide the Wi-Fi credentials there is also option to set the Server URL. Use the local address of LaraPaper.
  6. The device should automatically appear in the device list; you can deactivate the "Permit Auto-Join" toggle again.
  7. In the devices list, activate the toggle "☁️ Proxy" for your device. (Make sure that the queue worker is active. In the docker image it should be running automatically.)
  8. As long as no LaraPaper plugin is scheduled, the device will show your cloud plugins.
Troubleshooting

Make sure that your device has a Developer license, you should be able to verify by calling the https://trmnl.app/api/display endpoint.

🖥️ Generate Screens

Markup via Web Interface

  1. Navigate to Plugins > Markup in the Web Interface.
  2. Enter your markup manually or select from the available templates.
  3. Save and apply the changes.

🎨 Blade View

php artisan trmnl:screen:generate

Generate via API

You can dynamically update screens by sending a POST request.

  • Send a POST request to /api/screen with the following payload
Header

Authorization Bearer <TOKEN>

Body
{
    "markup": "<h1>Hello World</h1>"
}

Releated Work

🤝 Contribution

Contributions are welcome! See CONTRIBUTING.md for details.

License

MIT