What it's for App

Docs

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. Every feature, with working examples for both the CLI and the raw API.

LuisSystemd is a real multi-tenant platform. Everything below is scoped per-account and per-project — nobody can see or touch another user's projects, files, or secrets.
Jump to a section

Quickstart

Deploy a real server from the command line in under a minute.

1

Install the CLI:

shellcurl -fsSL https://luisystemd.pages.dev/install | sh
2

Create an account:

shelllsd signup
3

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 Files tab — 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 — tapping 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:

  1. main.py → run with python3
  2. main.js → run with node
  3. worker.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.

Auto-install is best-effort, not all-or-nothing: if one line in 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.

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.

Share vs. Deploy: no server-side logic → Share. Any endpoint that computes, calls out, or reads a secret → Deploy.

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 — tap it in the Files tab to see its tables (name, columns, row counts), tap 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.

Tap + 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.

The AI assistant only ever sees secret names, never values. Ask it to "use my Stripe key" and it writes 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

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)
Never put secrets in .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. Also shows deploy count, last-deployed time, and both live URLs (server + static) with one-tap 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; tap 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.

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.

CommandWhat it does
lsd login / lsd signupAuthenticate, store the API key locally
lsd init [--template ID]Create a project and link this directory
lsd pushUpload local files, sync .lsdw
lsd deployPush, then deploy as a real server
lsd statusLive compute status
lsd logs [-f]Show or follow deploy logs
lsd history / lsd rollback <id>List and restore snapshots
lsd secrets set/list/deleteManage encrypted secrets
lsd jobs add/list/rm/run/enable/disableManage scheduled jobs
lsd share / lsd unshareStatic file hosting
lsd deleteDelete the linked project

API reference

Every CLI command and every UI action calls this same REST API. All authenticated endpoints take an Authorization: Bearer <api_key> header.

POST /api/account/signup
shellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/account/signup \
  -H "Content-Type: application/json" -d '{"username":"...","password":"..."}'
POST /api/account/login
shellcurl -X POST https://shoes-predicted-wanted-sets.trycloudflare.com/api/account/login \
  -H "Content-Type: application/json" -d '{"username":"...","password":"..."}'
GET /api/projects
shellcurl https://shoes-predicted-wanted-sets.trycloudflare.com/api/projects -H "Authorization: Bearer $LSD_KEY"
POST /api/project/create
shellcurl -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"}'
POST /api/fs/write
shellcurl -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)"}'
POST /api/deploy
shellcurl -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}
POST /api/compute/status
shellcurl -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"}'
POST /api/project/secrets/set
shellcurl -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_..."}}'
POST /api/jobs/create
shellcurl -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.

lsd delete needs an interactive terminal

It asks for a typed yes confirmation, so it can't be scripted or piped non-interactively. 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"}'
Note the body key here is id, not project_id — inconsistent with most other project endpoints. Sending project_id silently resolves to an empty id and fails with {"error": "Only the owner can delete this project"} even when you are the owner.

lsd logs -f can print nothing while lsd logs works fine

If -f looks stuck, fall back to polling with plain lsd logs:

shellwatch -n 2 lsd logs

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.

Architecture notes →