Skip to main content

Serve all your web apps from a single process — they don't import it, they don't know it's there

Project description

enlace

Serve all your web apps from a single command. They don't import it. They don't know it's there.

Tired of launching a separate server for each app? Register your apps with enlace — point it at where they live — and serve them all with one command. Python ASGI apps get mounted in-process. Non-Python apps (Node.js, Go, etc.) get spawned as supervised child processes and routed via reverse proxy. External services and static sites work too. Your apps stay independent — no code changes, no shared dependencies. For more details, see the Philosopy section.

Quick start

The quickest way to start is to use enlace via AI. That's what we'll demo here. For those want to work in CLI or python, see the late Under the hood sectiob.

Install

pip install enlace

Skills are bundled with the package. To make them available to Claude Code:

# Link enlace's skills into your project (or globally)
skill link-skills "$(python -c 'import enlace; print(enlace.__path__[0])')"

# Or symlink manually
ln -s "$(python -c 'from enlace import skills_dir; print(skills_dir())')"/* .claude/skills/

Using enlace with an AI agent

enlace ships with AI agent skills that let Claude Code (or any compatible agent) handle the entire workflow through natural language:

"Add my_app to the platform"
"Can my_app be enlaced?"
"Diagnose /path/to/my_app and fix what you can"
"List my apps"
"List the apps configurations"
"Serve all my apps"

Available skills

Skill What it does Trigger phrases
enlace Create apps, configure platform.toml, understand conventions, serve "add an app", "serve my apps", "configure enlace"
enlace-diagnose Analyze an app for compatibility, suggest fixes that preserve standalone operation "can this be enlaced?", "diagnose this app", "what needs to change?"
enlace-dev Modify the enlace package itself — add features, fix bugs, extend middleware "add X to enlace", "implement Y in enlace"

What the AI does for you

Onboarding an existing app: The agent runs enlace diagnose, reads the report, and presents findings in three tiers: enlace-side fixes (no app changes), app changes that preserve standalone, and warnings. It proposes specific code changes and applies them with your approval.

Creating a new app: The agent scaffolds the directory structure, writes server.py with a FastAPI app, optionally creates frontend/index.html, registers it in platform.toml, runs enlace check, and starts serving.

Day-to-day operations: The agent runs enlace serve, enlace check, enlace show-config as needed, interprets the output, and explains what's happening.

Philosophy

Apps don't depend on enlace. enlace is an operator's tool, not a library your app imports. Your app is a standard Python module with app = FastAPI(). It runs standalone with uvicorn server:app. enlace just happens to know how to find it, mount it at a route prefix, and serve it alongside other apps. (See Zero Coupling for how enlace provides services like auth and storage without creating a dependency.)

enlace (the platform)          your app (Python, Node, Go, ...)
├── fastapi                    ├── fastapi (or express, gin, ...)
├── uvicorn                    ├── pandas (or whatever you need)
├── pydantic                   └── ... your domain libs
├── argh
└── httpx (optional, for proxy)
                               ← no arrow here: your app does NOT import enlace

Two principles in tension:

  1. Apps should not need to change. All aggregation logic lives in enlace. When an app is hard to mount, prefer enlace-side config (app.toml, env vars) over app code changes.

  2. Enlaced apps must still work alone. When changes are suggested, they preserve standalone operation. The pattern: env-var with current value as default — standalone uses the original, enlace overrides at build time. (See Standalone Preservation for the env-var-with-default pattern and fix classification.)

For the full rationale — including how these principles interact, where the balance sits today, and what enlace aspires to handle better — see the Design Principles document.

Under the hood

For those who want direct control, here's the CLI, Python API, and configuration system that the skills use internally.

Simple FastAPI example

pip install enlace
# Create an app
mkdir -p apps/hello
cat > apps/hello/server.py << 'EOF'
from fastapi import FastAPI
app = FastAPI()

@app.get("/greet")
def greet(name: str = "world"):
    return {"message": f"Hello, {name}!"}
EOF

# Serve it
enlace serve
# → http://localhost:8000/api/hello/greet?name=Thor

CLI

enlace serve              # Start backend (dev mode, hot reload)
enlace show-config        # Resolved config with provenance
enlace check              # Validate config, check route conflicts
enlace list-apps          # Table: name, route, type, access
enlace diagnose <dir>     # Analyze an app for enlace compatibility

Python API

from enlace import diagnose_app, discover_apps, build_backend

# Diagnose an app
report = diagnose_app("/path/to/my_app")
print(report)              # Human-readable report
print(report.is_enlaceable)  # True if no critical blockers

# Discover and compose
config = discover_apps()
app = build_backend(config)  # FastAPI app with all sub-apps mounted

App discovery

enlace discovers apps by filesystem conventions:

apps/
├── my_tool/
│   └── server.py          # has `app = FastAPI()` → mounted at /api/my_tool
├── dashboard/
│   ├── server.py           # backend
│   └── frontend/
│       └── index.html      # served at /dashboard/
├── calculator/
│   └── server.py           # typed functions, no `app` → auto-wrapped as routes
├── blog_node/
│   ├── app.toml            # mode = "process", command = ["node", "server.js"]
│   └── server.js           # spawned + proxied at /api/blog_node
└── docs/
    ├── app.toml            # mode = "static", public_dir = "dist"
    └── dist/index.html     # served directly
Convention Default Override in app.toml
Serving mode asgi mode (asgi, process, external, static)
Route prefix /api/{dir_name} route
Entry point First of server.py, app.py, main.py entry_point
ASGI app object Attribute named app app_attr
Frontend assets frontend/index.html frontend_dir

Everything enlace infers is inspectable (enlace show-config --verbose) and overridable via app.toml, platform.toml, environment variables, or CLI flags.

Configuration

platform.toml (project root):

[platform]
apps_dirs = ["apps"]                # Directories containing app subdirs
app_dirs = ["/path/to/standalone"]  # Individual app directories
backend_port = 8000

[conventions]
entry_points = ["server.py", "app.py", "main.py"]
app_attr = "app"
frontend_dir = "frontend"

app.toml (per-app, in app directory):

# Python ASGI (default mode — just overrides)
route = "/api/custom-route"
entry_point = "backend/main.py"
access = "public"
display_name = "My App"
# Non-Python or separate process
mode = "process"
command = ["node", "server.js"]
port = 3001
route = "/api/blog"
# External upstream
mode = "external"
upstream_url = "http://192.168.1.50:3000"

For process/external modes: pip install enlace[process]

Override precedence (lowest → highest):

defaults → filesystem conventions → app.toml → platform.toml → env vars → CLI flags

App modes

Mode Description How it works
asgi (default) Python ASGI apps Imported + mounted on gateway FastAPI
process Any app as a child process Spawned, health-checked, reverse-proxied
external Pre-existing upstream Proxied, no lifecycle management
static Static file directory Served directly

Within asgi mode, apps are further classified:

Type How detected How mounted
asgi_app Module has callable app attribute parent.mount(prefix, sub_app)
functions No app attr, has typed public functions Auto-wrapped as API routes
frontend_only No backend entry, has frontend/index.html Static file serving only

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

enlace-0.0.4.tar.gz (185.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

enlace-0.0.4-py3-none-any.whl (61.9 kB view details)

Uploaded Python 3

File details

Details for the file enlace-0.0.4.tar.gz.

File metadata

  • Download URL: enlace-0.0.4.tar.gz
  • Upload date:
  • Size: 185.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for enlace-0.0.4.tar.gz
Algorithm Hash digest
SHA256 ef7e29b91a271d8aab0479222cb642768f83cec39932be38ce81cd2b08595f5f
MD5 ed70f951d84cf3eaa4807b7e4db9326f
BLAKE2b-256 314536da3edf7cb58422e972df5f1c3fab5d1b3fe28f98e6a9136581ce1c6950

See more details on using hashes here.

File details

Details for the file enlace-0.0.4-py3-none-any.whl.

File metadata

  • Download URL: enlace-0.0.4-py3-none-any.whl
  • Upload date:
  • Size: 61.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for enlace-0.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 a776b25d51f8a8ba5d249c5f92c76d1892f87a9ec291d2e17b4c1afad0b801f8
MD5 542da0d4a5c2dea717bd73b1e6657f27
BLAKE2b-256 3cabf03f684831fdef54560ea9c97662b235fbc96efa9856b64838c0d5b94b43

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page