A self-hosted iOS springboard-style dashboard for your home services. Organize apps, bookmarks and widgets on a paginated grid with drag-and-drop, search and full JSON persistence.


- Paginated grid - swipe or arrow-key between pages, free-placement coordinate layout.
- Drag & drop - reorder apps across pages with live drop preview.
- Search - full-screen instant search overlay.
- App management - add, edit, delete apps from the web UI. Drop an external URL onto the grid to auto-create an app with its favicon.
- Folders - group apps into folders via a modal overlay.
- Widgets - load custom widgets from the
gallery/directory. Supports JSON and TSX widget formats, rendered in shadow DOM. - Uploads - custom icons and wallpapers uploaded through the settings panel.
- Themes - glassmorphism UI, icon shape selection (square, rounded, circle), adjustable grid spacing.
- Weather chip - optional weather display with city-based lookup (toggleable).
- Localization - persistent EN / FR switch.
- PWA - installable on iPhone / Android home screen with proper icons and manifest.
- Backup - full export / import with base64-encoded assets and automatic daily snapshots (retention: 10).
- Dashboard branding - customizable dashboard name, page title, and favicon.
| Layer | Technology |
|---|---|
| Frontend | React 18, Vite 5 |
| Backend | Express 4, Node.js 20+ |
| Persistence | JSON file (data/dashboard.json) |
| Uploads | Multer → public/uploads/ |
| Widgets | Shadow DOM runtime, TSX/JSON |
home_dashboard/
├── server/
│ ├── index.js # Express API + static file serving
│ └── storage.js # JSON persistence, backup, gallery management
├── src/
│ ├── App.jsx # Main dashboard state & rendering
│ ├── i18n.js # EN/FR translations
│ ├── lib/
│ │ └── api.js # Client-side API wrappers
│ └── components/
│ ├── AppTile.jsx
│ ├── FolderOverlay.jsx
│ ├── FolderTile.jsx
│ ├── SearchOverlay.jsx
│ ├── SettingsModal.jsx
│ └── ShadowWidget.jsx
├── gallery/ # Widget source files (JSON, TSX)
├── public/
│ ├── icons/ # PWA icons
│ ├── uploads/ # User-uploaded icons & wallpapers
│ ├── manifest.webmanifest
│ └── sw.js # Service worker (app shell only)
├── data/
│ └── dashboard.json # Persisted dashboard state
├── backups/ # Automatic daily snapshots
├── Dockerfile
├── docker-compose.yml
├── vite.config.js
├── package.json
└── index.html
No need to clone the repository. Just create a docker-compose.yml anywhere on your machine:
services:
limb:
image: ghcr.io/infinition/limb:latestcontainer_name: limbrestart: unless-stoppedports:
- "8090:3001"volumes:
- ./data:/app/data
- ./backups:/app/backups
- ./gallery:/app/gallery
- ./uploads:/app/public/uploadsenvironment:
- NODE_ENV=production
- PORT=3001Then run:
docker compose up -dThat's it. The dashboard is accessible at http://<host>:8090.
To update to the latest version:
docker compose pull && docker compose up -dOptional: automatic updates with Watchtower
Add this service to your docker-compose.yml to automatically pull new versions every 5 minutes:
watchtower:
image: containrrr/watchtowercontainer_name: watchtower-limbrestart: unless-stoppedvolumes:
- /var/run/docker.sock:/var/run/docker.sockenvironment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=300command: limbNAS / custom volume paths
If your data directory is elsewhere (e.g. Synology NAS), adjust the volume paths:
volumes:
- /volume1/docker/limb/data:/app/data
- /volume1/docker/limb/backups:/app/backups
- /volume1/docker/limb/gallery:/app/gallery
- /volume1/docker/limb/uploads:/app/public/uploadsFor contributors who want to work on the code:
git clone https://github.com/infinition/limb.git
cd limb
npm install
npm run devThis starts both the Express API on http://localhost:3001 and the Vite dev server on http://localhost:5173.
| Variable | Default | Description |
|---|---|---|
PORT | 3001 | Server listening port |
NODE_ENV | - | Set to production for optimized serving |
| Method | Endpoint | Description |
|---|---|---|
GET | /api/dashboard | Fetch dashboard state + widget library + assets |
PUT | /api/dashboard | Save dashboard state |
POST | /api/upload/icon | Upload an icon |
POST | /api/upload/wallpaper | Upload a wallpaper |
DELETE | /api/upload/icon/:name | Delete an icon |
DELETE | /api/upload/wallpaper/:name | Delete a wallpaper |
GET | /api/widgets/gallery | List available widgets |
POST | /api/widgets/gallery | Save a widget to the gallery |
POST | /api/widgets/import | Import a widget file |
DELETE | /api/widgets/gallery/:file | Delete a widget |
PUT | /api/widgets/:id/state | Save widget instance state |
GET | /api/backup/export | Download full backup bundle |
POST | /api/backup/import | Restore from a backup bundle |
Place a .json or .tsx file in the gallery/ directory.
JSON format:
{
"name": "My Widget",
"description": "A simple counter",
"defaultW": 2,
"defaultH": 2,
"html": "<div id=\"root\">0</div>",
"css": "#root { font-size: 2rem; text-align: center; }",
"js": "let c=0; document.getElementById('root').onclick=()=>{document.getElementById('root').textContent=++c;}"
}Widgets can also be imported directly from the Settings panel.
MIT