# How it works
sharepack is a four-stage build pipeline plus a small browser runtime.
## The pipeline
1. **Collect** — walk the project directory. Source, templates, config,
static assets (images, fonts, stylesheets), and SQLite databases are kept;
caches, virtualenvs, logs, and VCS directories are ignored. Secrets are
scrubbed (below). Everything kept is read as bytes.
2. **Detect** — find the framework. The Django adapter looks for `manage.py`
and reads the settings module out of it (`--settings-module` overrides
this); it also parses `STATIC_URL` and warns about non-SQLite database
backends. The Flask and FastAPI adapters scan entry modules (`app.py`,
`main.py`, …) for an app instantiation (`--app module:variable`
overrides). All adapters warn about incompatible dependencies found in
`requirements.txt`.
3. **Encode** — base64 every file into one JSON payload.
4. **Emit** — splice the payload, a Python boot script, and framework
metadata into an HTML template. The output is one self-contained file.
## What gets scrubbed and skipped
At build time, before anything is embedded:
- `.env`, `.env.local`, `.env.production`, `.env.development` are excluded.
- Files whose *names* look credential-like (`secret`, `token`, `password`,
`credential`, `apikey`, `api_key`) are excluded, unless they are `.py` or
`.html` source files.
- `SECRET_KEY = "..."` in `settings.py` is replaced with a throwaway value.
- Files with extensions sharepack doesn't recognize are skipped **and
reported** — run `--dry-run` to see the full list with reasons, and use
`--include`/`--exclude` globs to adjust.
Every scrub and skip is printed in the build report. Nothing is silently
dropped.
## The browser runtime
When the recipient opens the file:
1. The page loads the Pyodide runtime (~10 MB, cached by the browser) from
the jsDelivr CDN and installs Django with micropip from PyPI. This is the
only time the network is used.
2. The boot script writes every bundled file into the WASM virtual
filesystem at `/app/` and boots the framework through a server-free
transport:
- **Django** — `django.setup()` plus `django.test.Client`, Django's own
request machinery repurposed as the transport.
- **Flask** — the app's `test_client()`, whose built-in cookie jar makes
sessions and flash messages work.
- **FastAPI** — a minimal in-process ASGI driver (the usual test client
needs threads, which WebAssembly doesn't have): it builds the request
scope by hand, follows redirects, and keeps its own cookie jar so
sessions survive. `anyio`'s threadpool is patched to run inline.
3. Every link click and form submit is intercepted in JavaScript and routed
through that transport. The response HTML is rendered into an iframe.
There is no port, no socket, and no server anywhere.
### Static files
References to `STATIC_URL` paths in rendered HTML (``,
``, script tags) are resolved through Django's
staticfiles finders inside the WASM filesystem, converted to blob URLs, and
rewritten before the page is displayed. Stylesheets, images, and fonts work
without `collectstatic`.
### Content types
Responses are dispatched on their content type: HTML renders normally, JSON
pretty-prints, plain text renders as text, images display, and anything
binary becomes a download link.
## Honest caveats
The artifact is a **demo**, not a deployment:
- CSRF checks are disabled and `DEBUG = True` is forced at boot, so form
posts work and errors are readable. Never treat the bundle as a hardened
build of your app.
- Anyone who has the file has your bundled code and data. The scrubber
removes obvious secrets, but review `--dry-run` output before sharing
something sensitive.
- Each recipient gets a private copy. Writes persist across clicks but reset
on reload; viewers never see each other's changes.
## Prior art
[WordPress Playground](https://wordpress.github.io/wordpress-playground/)
(the pattern), [stlite](https://github.com/whitphx/stlite) (same idea for
Streamlit), and
[django_webassembly](https://github.com/m-butterfield/django_webassembly)
(the proof of concept this productizes).