LuisSystemd
Host a real, always-on server, a SQLite database, and a terminal under your own account — running on shared infrastructure instead of your own hardware. This page documents every feature, with working examples for both the CLI and the raw API.
Quickstart
Deploy a real server from the command line in under a minute.
Install the CLI:
shellcurl -fsSL https://luisystemd.pages.dev/install | sh
Create an account:
shelllsd signup
Spin up a project from a template and deploy it:
shellmkdir my-api && cd my-api
lsd init --template flask-api
lsd deploy
That's it — lsd deploy prints a live public URL. Dependencies (requirements.txt/package.json) install automatically before your server starts.
Accounts
Sign up with a username and password at the top of the app, or via lsd signup. Each account gets its own API key (prefixed lsd_...), used to authenticate every request. There's no relationship between a LuisSystemd account and accounts on other Luis products — it's a separate system.
Example — signup via curl
shellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/account/signup \
-H "Content-Type: application/json" \
-d '{"username":"yourname","password":"yourpassword"}'
# -> {"username":"yourname","key":"lsd_..."}
Creating projects
A project is a persistent workspace: files, an optional database, an optional running server, all tied to one directory on the backend. Create one from the + button in the sidebar (desktop) or Files tab (mobile) — pick a blank project or start from a template.
CLI
shelllsd init --name "my project"
Templates
Four starting points, available from the new-project menu or lsd init --template <id>:
🐍 flask-api
A Python HTTP API using Flask, with a requirements.txt already set up.
🟢 node-express
A Node.js API using Express, with package.json already set up.
⚡ worker
A Cloudflare-Workers-style worker.js — just a fetch handler, no server boilerplate.
🌐 static-html
A single self-contained HTML page for the Share (static) flow.
shelllsd init --template node-express --name "my api"
lsd templates # list all available templates
Files
Every project has a real file tree on its backend workspace. Create, open, edit, and delete files and folders from the Files tab. .db files are handled specially — clicking one opens the database browser instead of the text editor.
CLI
shell# edit files locally, then:
lsd push
Deploy (real servers)
Deploy runs your project as a genuine, always-on process — not just static files. It looks for an entry point in this order:
main.py→ run withpython3main.js→ run withnodeworker.js→ wrapped in a Workers-style runtime shim (see below)
Your app's port is injected automatically as the PORT environment variable — read it, don't hardcode a port. The result is tunneled out to a real public URL.
Auto-install
If requirements.txt or package.json exists, dependencies install automatically before your server starts — no manual pip/npm step needed. No manifest file at all (a plain stdlib-only script) skips this step entirely rather than erroring.
Auto-restart
Optional per-project setting — if enabled, a crashed server respawns automatically (2s backoff) instead of staying down.
requirements.txt fails (a typo'd package name, a version that doesn't exist), the install step prints the pip error but deploy still proceeds and starts your server with whatever did install successfully. Useful if your app only actually needs a subset of what's listed, but worth knowing it won't hard-fail the deploy on a bad line.CLI
shelllsd deploy
lsd status # check if it's live, see the URL and process stats
lsd undeploy # stop it
worker.js (Cloudflare-Workers-style)
Instead of writing HTTP server boilerplate, add a worker.js with just a fetch handler:
javascriptmodule.exports = {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/json") {
return Response.json({ ok: true });
}
return new Response("hello", { status: 200 });
}
};
Deploy detects it automatically (checked after main.py/main.js) and wraps it in a small Node shim that adapts Node's http module to that API — you get request.method, request.url, request.headers.get(), request.json()/.text(), and a Response/Response.json() constructor.
Share (static files)
Serves your project's files exactly as they sit on disk — no code runs. Good for plain HTML/CSS/JS with no backend logic. If there's no index.html, there's nothing to show.
CLI
shelllsd share
lsd unshare
Terminal
A real, persistent shell in your project's workspace, available in the advanced panel. Commands run in the same directory Deploy uses, so pip install -r requirements.txt --break-system-packages or npm install run here are visible to your deployed server too — though Deploy already does this automatically for you on every deploy.
Database (SQLite)
Any .db file in your workspace can be browsed and queried directly — click it in the Files tab to see its tables (name, columns, row counts), click a table to auto-run SELECT * FROM table LIMIT 100, or write your own SQL.
Example — create a table via curl
shellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/db/query \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" \
-d '{"project_id":"$PID","db_file":"data.db","sql":"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"}'
Environment variables
Plain, non-secret configuration — feature flags, log levels, base URLs. Set them in the Compute Engine tab or via .lsdw. Injected into your process alongside PORT at deploy time.
Secrets
API keys and other sensitive values, encrypted at rest (AES via Fernet) and never shown back in full — only masked (sk_l••••••ijkl). Decrypted only server-side, only into your deployed process's environment.
Click + Secret for a searchable preset list (OpenAI, DeepSeek, Anthropic, Stripe, GitHub, AWS, and more), or add a custom key. If a secret and an env var share the same name, the secret wins — verified directly: setting both env.STRIPE_SECRET_KEY=placeholder in .lsdw and a real secret of the same name, the deployed process only ever sees the secret's value, never the env var's.
os.environ["STRIPE_SECRET_KEY"] — the real value is injected outside its context entirely.CLI (value prompted securely, never in shell history)
shelllsd secrets set STRIPE_SECRET_KEY
lsd secrets list
Scripting / CI (value as an argument instead of a prompt)
lsd secrets set takes the value as an optional second positional argument — omit it to be prompted (recommended for interactive use, keeps it out of shell history), or pass it directly when scripting a setup step:
shelllsd secrets set STRIPE_SECRET_KEY "$STRIPE_KEY_FROM_CI_SECRET_STORE"
.lsdw manifest
A plain-text project manifest, read by the CLI (and pushed to via lsd push):
text# .lsdw -- LuisSystemd project manifest, plain key=value lines.
project_id=<id> # set automatically by `lsd init` / `lsd link`
auto_restart=true|false # restart the server automatically if it crashes
env.KEY=value # a non-secret environment variable (repeatable)
.lsdw — it's meant to be committed alongside your code. Use lsd secrets set KEY instead.Compute Engine
Real, live stats for your project: CPU load, memory, disk (shared VM-wide, labeled honestly as such), plus per-process stats for your specific deployed server — its actual worker PID, RSS memory, and process uptime (found by walking the process tree, not just reporting the launcher's own numbers). Also shows deploy count, last-deployed time, and both live URLs (server + static) with one-click copy.
CLI
shelllsd status
Deploy history & rollback
Every deploy snapshots your workspace (a tarball, keeping the last 10) before applying the new deploy. Roll back to restore an older snapshot's files — this only restores files, it doesn't redeploy by itself; click Deploy again afterward to make the restored version live.
CLI
shelllsd history
lsd rollback <version-id>
lsd deploy --no-push # make the restored version live
Scheduled jobs
Run a shell command in your project's workspace on a repeating interval (minutes) — backups, scrapes, cleanup, anything that doesn't need to be an always-on server. A background scheduler checks for due jobs every 30 seconds. Each job tracks its last run time, exit code, and output; supports manual "Run now," enable/disable, and delete.
CLI
shelllsd jobs add "python3 backup.py" --every 60
lsd jobs list
lsd jobs run <job-id>
AI assistant
Describe what you want built — the AI writes files directly into your current project. It's told which env vars and secrets already exist (names only) so it can reference them correctly instead of inventing values or asking you to paste them into chat.
CLI (lsd)
A full command-line client, talking to the same API the web app uses.
| Command | What it does |
|---|---|
lsd login / lsd signup | Authenticate, store the API key locally |
lsd init [--template ID] | Create a project (blank or from a template) and link this directory |
lsd push | Upload local files, sync .lsdw env vars/auto_restart |
lsd deploy | Push, then deploy as a real server |
lsd status | Live compute status for the linked project |
lsd logs [-f] | Show or follow deploy logs |
lsd history / lsd rollback <id> | List and restore deploy snapshots |
lsd secrets set/list/delete | Manage encrypted secrets (prompts via getpass, never in shell history) |
lsd jobs add/list/rm/run/enable/disable | Manage scheduled jobs |
lsd share / lsd unshare | Static file hosting |
lsd delete | Delete the linked project |
API reference
Every CLI command and every UI action calls this same REST API. All authenticated endpoints take a Authorization: Bearer <api_key> header. The CLI already knows this URL by default (lsd login needs only a username and password) — these examples use it directly so you can copy-paste them as-is.
Setting the header in an API client — lsd_... is your real API key from lsd login / lsd signup, not a placeholder to fill in literally.
/api/account/signupshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/account/signup \
-H "Content-Type: application/json" -d '{"username":"...","password":"..."}'
/api/account/loginshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/account/login \
-H "Content-Type: application/json" -d '{"username":"...","password":"..."}'
/api/projectsshellcurl https://shoes-predicted-wanted-sets.trycloudflare.com/api/projects -H "Authorization: Bearer $LSD_KEY"
/api/project/createshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/project/create \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" -d '{"name":"my project"}'
/api/fs/writeshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/fs/write \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" \
-d '{"project_id":"$PID","path":"main.py","content":"print(1)"}'
/api/deployshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/deploy \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" -d '{"project_id":"$PID"}'
# -> {"url":"http://bore.pub:...","runtime":"python3","entry":"main.py","install_ran":true}
/api/compute/statusshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/compute/status \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" -d '{"project_id":"$PID"}'
/api/project/secrets/setshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/project/secrets/set \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" \
-d '{"project_id":"$PID","secrets":{"STRIPE_SECRET_KEY":"sk_..."}}'
/api/jobs/createshellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/jobs/create \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" \
-d '{"project_id":"$PID","command":"python3 backup.py","interval_minutes":60}'
Troubleshooting
Things that came up while stress-testing every feature end-to-end — real behavior, not hypotheticals.
Template id typos fail with a generic error
lsd init --template workers (plural) fails with error: Unknown template — the real id is worker (singular). Run lsd templates first if unsure; the error doesn't currently suggest the closest match.
lsd delete needs an interactive terminal
It asks for a typed yes confirmation, so it can't be scripted or piped non-interactively — running it from a non-TTY context fails with EOFError: EOF when reading a line. For automation, call the API endpoint directly instead:
shellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/project/delete \
-H "Authorization: Bearer $LSD_KEY" -H "Content-Type: application/json" \
-d '{"id":"$PID"}'
id, not project_id — inconsistent with most other project endpoints (/api/project/create, /api/compute/status, etc., which all use project_id). Sending project_id to this endpoint silently resolves to an empty id and fails with {"error": "Only the owner can delete this project"} even when you are the owner — the actual cause is the wrong key, not a permissions issue.lsd logs -f can print nothing while lsd logs works fine
In testing, generating real traffic against a deployed server while lsd logs -f was attached produced zero output for the whole session, even though the exact same requests showed up correctly and in order with a plain (non-follow) lsd logs call afterward. The logs themselves aren't lost — the backend recorded everything — it's specifically the CLI's streaming/follow display that can fail silently. If -f looks stuck, fall back to polling with plain lsd logs:
shellwatch -n 2 lsd logs # re-poll every 2s in your terminal
# or keep a running file in the background instead of a live terminal:
nohup sh -c 'while true; do lsd logs >> deploy-log.txt; sleep 2; done' &>/dev/null &
tail -f deploy-log.txt # watch it grow
Secrets vs. env vars: secrets always win
If .lsdw sets env.SOME_KEY=... and a secret named SOME_KEY also exists, the deployed process only ever sees the secret's value. This was verified directly, not just documented behavior — worth knowing if a value doesn't seem to be updating after changing .lsdw: check whether a same-named secret is silently taking precedence.