This Lenny-app is maintained by the ArchiveLabs. which is client app example for Lenny
Turborepofor monorepo management.Nextjsinside Turborepo.Tailwindcssfor styling.Dockerfor deployment and containerization.Nginxfor reverse proxy.
This monorepo contains two Next.js applications:
- Web (
apps/web/) - Main web application - Docs (
apps/docs/) - Documentation site
Each app has its own Dockerfile and runs independently. A single Nginx reverse proxy routes traffic to both apps on separate ports.
- Web: http://localhost:3000/
- Docs: http://localhost:3001/
- Web: http://localhost:8080/
- Docs: http://localhost:8081/
- web - Next.js app running on port 3000 inside container
- docs - Next.js app running on port 3001 inside container
- nginx - Reverse proxy with two server blocks:
- Port 80 → proxies to
web:3000(exposed as host port 8080) - Port 81 → proxies to
docs:3001(exposed as host port 8081)
- Port 80 → proxies to
- Location:
docker/nginx.conf - Purpose: Reverse proxy with two separate server blocks for web and docs apps
- Features:
- Standard proxy headers for proper request forwarding
- WebSocket upgrade support for Next.js hot reload
- Each app served at root path
/on its own port - Debug header
X-Proxyto identify the Nginx instance
Each app has its own optimized multi-stage Dockerfile:
- Web:
apps/web/Dockerfile - Docs:
apps/docs/Dockerfile
-
Install dependencies:
pnpm install
-
Start the development servers:
pnpm run dev
This will start both apps:
- Web: http://localhost:3000/
- Docs: http://localhost:3001/
-
Make sure you have your own Lenny setup running on your machine (installation guide)
git clone git@github.com:ArchiveLabs/lenny.git cd lenny ./run.sh --public --preloadThis will add 800+ books inside your Lenny. Feel free to check the Github Docs for Lenny.
- Docker Engine 20.10+
- Docker Compose v2.0+
Build and start all services (web, docs, and nginx):
docker compose up --buildOr run in detached mode:
docker compose up --build -d# Build all services
docker compose build
# Build specific service
docker compose build web
docker compose build docs# Start all services
docker compose up
# Start in detached mode (background)
docker compose up -d
# Stop all services
docker compose down
# Stop and remove volumes
docker compose down -v# View all logs
docker compose logs
# Follow logs in real-time
docker compose logs -f
# View logs for specific service
docker compose logs web
docker compose logs docs
docker compose logs nginxOnce the containers are running, test the endpoints:
# Test web app
curl http://localhost:8080/
# Test docs app
curl http://localhost:8081/
# Or open in browser
open http://localhost:8080/
open http://localhost:8081/To change the exposed ports, edit compose.yaml:
nginx:
ports:
- "8080:80"
- "8081:81"Make sure you have your own Lenny setup running on your machine (installation guide)
curl -fsSL https://raw.githubusercontent.com/ArchiveLabs/lenny/refs/heads/main/install.sh | sudo shThis will add 800+ books inside your Lenny. Feel free to check the Github Docs for Lenny.
If you want to use the Lenny web app outside the Docker container (for local development), you need to allow local network IP addresses to upload files.
Add the following code inside the is_allowed_uploader function in code/api.py within your Lenny instance:
if client_ip.startswith("172.") or client_ip.startswith("192.168."):
return TrueThis allows upload requests from Docker networks (172.x.x.x) and local networks (192.168.x.x) to bypass the uploader IP restrictions during development.
We're seeking partnerships with libraries who would like to try lending digital resources to their patrons.
Notes for anyone upgrading a working install of apps/web (the admin UI served under /admin).
- App Access (Settings → Apps & readers → App Access,
/admin/settings/app-access): manage the reading apps and catalogs that may sign patrons in. Add an app, edit its name / redirect URLs / permissions, turn it on or off, reset a server app's secret, and remove an app that is turned off. A For developers tab shows the endpoints an app should use. The old/settings/connected-appsand/connected-appsURLs redirect there. - Patron Sign-in Provider: the former "External Auth (OIDC)" page, renamed only. Same route (
/settings/external-auth), same behaviour. It is the opposite direction from App Access: here Lenny signs patrons in with an outside provider; in App Access, apps use Lenny's sign-in. - Density and typography (
apps/web/app/density.css): the whole admin is scaled by one root font size that depends on the device. Touch screens: 15px on phones, 14px from 768px up. Mouse devices: 16px below 768px, 15px at 768–1023px, 14px at 1024–1919px, 15px at 1920–2559px, 16px from 2560px. Inputs stay 16px and touch controls stay at least 44px tall. The main column is capped at 84rem. Tune the--root-size-*variables in that file; nothing else needs to change. - Security hardening of the admin proxy and login (see below).
- The admin proxy (
/api/admin/*) is stricter. Paths containing.or..segments, encoded slashes,?,#, backslashes or control characters are rejected with 400. A request without theadmin_tokencookie gets 401 from the proxy itself (the internal secret is never attached to anonymous requests), including paths that end in.json,.png,.svgor.ico. Upstream redirects are not followed (a 3xx from Lenny becomes a 502). - Origin check on state-changing calls.
POST/PUT/PATCH/DELETEto/api/admin/*and the upload route are rejected with 403 when the browser sendsSec-Fetch-Site: cross-site, or anOriginwhose hostname differs fromX-Forwarded-Host(orHost). Ports are ignored. A reverse proxy in front of the admin app must forward the originalHost(orX-Forwarded-Host). Both known setups do: lenny-app's owndocker/nginx.confsendsHost $host(no port), and Lenny'sdocker/nginx/conf.d/lenny.confsendsHost $http_host(with port) for/admin; the hostname comparison accepts either. A proxy that rewritesHostto an internal name (for examplelenny_admin:4000) without sendingX-Forwarded-Hostwould make every changing call return 403. Login and logout are not covered by this check. - Login fails closed in production. With
NODE_ENV=production, ifLENNY_INTERNAL_API_URLorADMIN_INTERNAL_SECRETis unset or empty, login returns 503. Before, it fell back to the development credentials. This only affects a standalone install of lenny-app that you run with lenny-app's owncompose.yaml(itswebservice sets neither variable): provide both (for example withenvironment:or anenv_file:) before upgrading. Installs run through Lenny's owncompose.yamlalready pass both to the admin container (LENNY_INTERNAL_API_URLis set there andADMIN_INTERNAL_SECRETcomes fromauth.env), so nothing changes for them. AUTH_BYPASS=trueis ignored in production. It only works whenNODE_ENVis notproduction.- Cookies.
admin_tokenis HttpOnly andSameSite=strict, and is markedSecurewhenNODE_ENV=production, so production must be served over HTTPS (browsers do not store Secure cookies from plain HTTP, except on localhost). - Security headers. All admin responses now send
Content-Security-Policy: frame-ancestors 'none'(plusbase-uri,form-action,object-src),X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: same-originand a restrictivePermissions-Policy. The admin can no longer be embedded in an iframe. There is no script CSP yet. - Session expiry. On the App Access screen an expired admin session (401) clears the cookie and returns to the login page.
- Errors. Failures that carry no usable message (for example an HTML error page from a proxy) now show a short generic message instead of the raw body.
| Variable | Used for |
|---|---|
NEXT_PUBLIC_API_URL |
Public Lenny URL for client-side calls (public catalog). Not secret. |
LENNY_INTERNAL_API_URL |
Lenny API base for the admin proxy and login, e.g. http://lenny_api:1337/v1/api. Server-side only. Required in production. |
ADMIN_INTERNAL_SECRET |
Shared secret the proxy sends to Lenny. Server-side only. Required in production. |
ADMIN_USERNAME, ADMIN_PASSWORD |
Development login only, used when the two variables above are unset and NODE_ENV is not production. |
AUTH_BYPASS |
Development only: skips the login check. |
Never put a secret in a NEXT_PUBLIC_* variable; those are bundled into the browser.
App Access needs a Lenny API that provides the /admin/oauth2/clients endpoints (list, create, edit, reset secret, enable, disable, remove), that is, the Lenny release that introduced App Access. Against an older Lenny the rest of the admin keeps working and the App Access screen shows "This Lenny server doesn't support App Access yet." The Settings row for App Access then shows no summary. Upgrade Lenny first, then the admin app.