How StayPresent's web_server=False and bot-less run() modes work, plus the new exclude parameter for locking down static file exposure.
Running a Bot Without a Web Server (and Vice Versa) in StayPresent
Until v1.6.0, staypresent.run() always did both things at once: launch a bot, and run an HTTP server alongside it. That coupling made sense for the platform-hosting use case it was originally built for, but it didn't fit every deployment. Two new modes decouple these entirely, and a new exclude parameter tightens up a real security gap in static file serving. Here's how all three work.
Table of Contents
- Why Bot and Server Were Always Coupled Before
- Bot Supervision With No HTTP Server
- HTTP Server With No Bot at All
- When Each Mode Actually Makes Sense
- The
excludeParameter for Static Routes - How Exclusions Interact Across Multiple Routes
- Full Examples
- Best Practices
- Common Mistakes
- FAQs
- Conclusion
Why Bot and Server Were Always Coupled Before
StayPresent's original purpose was solving one specific problem: a bot that needs an HTTP port purely to satisfy a hosting platform's health check. Since that's the overwhelmingly common case, run() handled both concerns together by default. But not every deployment needs that pairing — sometimes you want process supervision with zero HTTP surface, or an HTTP status service with nothing running underneath it at all.
Bot Supervision With No HTTP Server
import staypresent
staypresent.run("bot.py", web_server=False)
This gives you StayPresent's full crash recovery and restart management — max_restarts, restart_delay, restart_reset_after, and (as of v1.6.0) heartbeat_timeout for hang detection — with zero HTTP server running at all. This is the right choice for a deployment where nothing external ever needs to reach an HTTP port: a background worker on infrastructure that doesn't impose a port-binding health check requirement, or a process where you're deliberately avoiding any network-facing surface for security reasons.
HTTP Server With No Bot at All
import staypresent
staypresent.run()
Called with no bot configuration at all, run() now spins up only the web server. This is genuinely new capability — previously, StayPresent's HTTP server only existed as a side effect of supervising a bot. Now it works as a standalone deployment on its own, which is specifically useful for a dedicated status/health-check service — for example, a central /status dashboard aggregating information that other, separate StayPresent-managed bot deployments report into, or simply a lightweight always-on health endpoint with nothing else attached to it.
When Each Mode Actually Makes Sense
web_server=False fits a bot running somewhere that genuinely doesn't need an HTTP port — a Docker container on a VPS with no platform-level health check, or a script you're intentionally keeping off the network entirely.
No bot configured (server-only) fits a dedicated monitoring/status deployment, or a lightweight service that's purely about serving web.json()/web.html()/web.markdown() content — a documentation page, a public API stub, a webhook receiver — with no subprocess supervision involved at all.
The default (both together) remains the right choice for the vast majority of bot deployments on Render, Railway, Koyeb, or Heroku, where the HTTP port requirement is the whole reason StayPresent exists in the first place.
The exclude Parameter for Static Routes
Earlier versions of web.html()/web.markdown() served every file in the target directory as a static asset, not just the ones actually referenced from the page — a deliberate tradeoff for zero-config convenience, but one with a real security cost if a .env or source file happened to live in that same directory. exclude closes that gap directly:
staypresent.web.html(
"templates/index.html",
exclude=[".env", ".git", "*.py", "secrets.json"],
)
Exact filenames, extensions, and glob patterns are all supported. A request for any excluded file now returns a clean 404, rather than the file's actual contents.
How Exclusions Interact Across Multiple Routes
If more than one registered route serves files out of the same physical directory, exclusions set on any one of them are now merged globally across all of them — closing a gap where a secondary route pointed at the same directory could previously be used to bypass another route's own exclusion list.
staypresent.web.html("shared/index.html", exclude=[".env"])
staypresent.web.markdown("shared/CHANGELOG.md", path="/changelog")
# .env is now excluded for BOTH routes, since they share a directory
Full Examples
Bot-only, no HTTP server, on a VPS:
import staypresent
staypresent.run(
"worker.py",
web_server=False,
restart_on_crash=True,
max_restarts=5,
heartbeat_timeout=30,
)
Server-only, dedicated status deployment:
import staypresent
staypresent.web.status(title="Fleet Status")
staypresent.run(port=8080)
Locked-down static serving:
import staypresent
staypresent.web.html(
"public/dashboard.html",
exclude=[".env", ".git", "*.py", "*.log"],
)
staypresent.run("bot.py")
Best Practices
- Use
excludeon everyweb.html()/web.markdown()call whose target directory isn't 100% dedicated to public-facing files — it costs one line and closes a real exposure gap. - Reach for
web_server=Falsespecifically when you know for certain nothing external needs to health-check this deployment — don't default to it on a PaaS platform that actually requires the port. - Use server-only mode (
run()with no bot) for status aggregation, not as a general-purpose web framework replacement — it's still built around the sameweb.*primitives, not full request routing.
Common Mistakes
-
Setting
web_server=Falseon a platform that actually requires an HTTP port, which reintroduces the exact "platform marks deployment unhealthy" problem StayPresent exists to solve in the first place. -
Assuming
excluderetroactively protects a directory you've already deployed without it. Add exclusions before the first deploy where sensitive files might sit alongside a served template, not after. - Forgetting that exclusions merge across routes sharing a directory, and being surprised when a file is blocked on a route where you didn't explicitly list it — this is the intended, safer behavior as of v1.6.0.
FAQs
Does web_server=False still give me hang detection?
Yes — heartbeat_timeout and hang detection are independent of whether an HTTP server is running; they only depend on the bot's own heartbeat() calls.
Can I combine server-only mode with web.status()?
Yes — this is one of the more natural use cases for server-only mode: a dedicated status dashboard with no bot of its own.
Does exclude support regex, or only glob patterns?
Glob-style patterns (*.py, secrets.*) and exact filenames — not full regex.
Conclusion
Decoupling the bot and the web server gives StayPresent real flexibility beyond its original single use case, while exclude addresses a static-file exposure concern directly rather than leaving it as something you have to solve through directory hygiene alone. Together, they make v1.6.0 a meaningfully more general-purpose tool than earlier releases, without changing anything about how the default, most common setup behaves.
pip install "staypresent[prod]"
Top comments (0)