How to Share a Streamlit App Privately
There are two ways to share a Streamlit app privately, and they are not close substitutes. Either you keep the Python process running on a host that runs Python and put a login in front of it, or you publish what the app shows as a page that only the people you name can open.
The second route is the one this post is about, because most of the time the people waiting on your link want to read a result, not drive a tool. Here is how that works, what you keep, and what you give up.
How to share a Streamlit app privately
The version that takes an afternoon rather than a weekend:
- Decide whether your readers need the live Python or just what it produced.
- If they need the result, have your assistant emit the same charts, tables and commentary as one page.
- Publish that page.
- Name the email addresses allowed to open it.
- Send the link once.
Nobody installs anything, nobody gets a login to your server, and there is no container to keep alive between readings. What you lose is the part of the app that only exists while Python is running, which is worth being honest about before you start.
Why a private Streamlit app is harder than a private page
Streamlit runs your script on a server and re-runs it when someone moves a widget. Every slider drag is a round trip to a process that has to be up. Making that private means putting authentication in front of a long-running server: a deploy, a container that stays warm, somewhere to keep sessions, and a login screen you maintain. That is a real piece of infrastructure, and it is why plenty of internal Streamlit apps end up either on the office network or on a URL that is quietly open to anyone who finds it. If you go that way, check what your host's default visibility actually is rather than assuming the URL is private because nobody has shared it.
SnapHost sits on the other side of that line. A published site is served as files, and the interactive parts run in the visitor's browser. A bundle can carry HTML, CSS, JavaScript, JSON, images, fonts, video and audio, and a single Markdown file, Jupyter notebook or PDF is rendered into a page at publish time. A .py file is not one of the types a bundle may hold, so zipping your app folder and publishing it comes back as Disallowed file type in bundle. That refusal is the honest answer to "can I host my Streamlit app here", and it is better than a page that publishes and then does nothing.
Do your readers need the app, or the answer?
This is the question that decides everything else, and it is worth asking about the app you have rather than the app you imagined.
Keep the Python if the app queries a live database on demand, runs a model per request, or lets people put in parameters you could not list in advance. A pricing tool where a salesperson types any deal shape is a real app and it needs a real server.
Publish a page if the app reads a dataset you refresh on a schedule, shows a set of charts and a table, and gets opened by four people who look at it and close it. That is what a lot of Streamlit apps settle into after the first week. The widgets were how you explored the data while you were building it, not how anyone else uses it.
Publishing what the app shows
You do not rebuild the app by hand. Point your assistant at the same script and ask it for one self-contained HTML page with the figures and the tables the app renders, and it will hand you back a file. Plotly and Altair both export to HTML that keeps hover, zoom and legend toggling, because that interactivity was always running in the browser rather than in Python.
If the analysis started in a notebook, you can skip the export entirely and publish the .ipynb file, which renders as a notebook rather than as a wall of code. The notebook post covers that route on its own, and the publishing docs list everything a page can be made from.
A front end you build with a bundler works too. Zip the output folder and publish that, and deep links like /costs keep resolving when someone refreshes on them instead of falling over.
The interactivity you keep, and the part that breaks
More survives the move than people expect. Filtering, sorting, tabs, a slider that recalculates in JavaScript, a chart that redraws on a click: all of that runs in the visitor's browser and is unaffected by there being no server behind it. Published pages can use localStorage and IndexedDB, each site on its own isolated origin, so a reader's chosen date range can still be there tomorrow. Browser dialogs like window.print() work, which covers the colleague who prints everything.
Two things to know before you publish. A service worker still runs as code, but its registration does not take effect, so an offline mode is not something to promise. And browsers key that stored state to the address the page is viewed on, so the same site opened on your custom domain and on its share link keeps two separate stores.
The one that actually bites is loading things from elsewhere. A published page's references to other sites are blocked by default, and an exported chart that pulls its JavaScript from a CDN is exactly that, so it renders half-empty. When you publish or update, the content is scanned and the references the policy will block are reported back to you. That scan is detect-only and best effort: it warns, it does not fix anything, and it never fails the publish. Either trust those specific origins for the site (a paid capability) or, better for a page you want to keep working in five years, ask your assistant to inline the chart library into the bundle.
When the app was collecting something
Plenty of Streamlit apps are not dashboards at all. They are a form with a chart above it: rate this output, label this row, pick a scenario and tell me which you prefer. Losing Python looks fatal for those, and it is not.
A published page can save and read its own records with no backend. One call appends to a named collection and another reads it back, and your assistant can wire both into the page it just built for you. Collecting data is a Pro feature, and the data docs have the recipes.
A new collection is private by default: visitors can add entries and only you can read them, which is the behaviour you want for a review queue. When you publish, a collection the page reads from is opened up so the page works, one it only submits to stays private, and you are told which was set up either way. It is meant for small structured things, form entries and ratings rather than files, and Pro includes 5 GB of it.
Who can open it
A private page is limited to a list of email addresses. When someone opens the link they confirm their address before any content loads, and only once they match the list is a short-lived delivery token minted and the page streamed out of private storage into a sandboxed frame. The stored files are never handed to the browser to fetch. Every request, verification, view and denial is recorded, so before a Monday meeting you can see who has opened it and who has not.
On Pro you can allow a whole domain, so everyone at acme.com is covered by one entry. On Free you list addresses one at a time. Either way the check runs again on every open, which is the thing a link in a shared drive does not do: a Drive link is not access control.
Keeping it current, and going back
Replacing the contents of the page keeps its address, so the link you sent in March still opens today's numbers and you never send a second link. That works on every plan. On Pro you can let your assistant refresh the page on a schedule, every Monday morning for instance. The numbers are then as fresh as your last run, which is as close to a live app as this route gets.
Each update is saved as a version and the ten most recent are kept, so when an assistant helpfully restyles a chart nobody asked it to touch, you roll back and the link does not change. Version history is on Pro.
What it costs
Free gives you three pages at a time, each either public or private to up to three named people, served from sites.snaphost.ai with a small SnapHost mark in the corner. For one dashboard and three readers that is a real answer rather than a trial.
Pro is 19 euros a month for each member who can publish, and readers are free however many of them there are. The data layer, domain entries, version history, scheduled refreshes and your own web address are all on that side of the line. The pricing page is the current list.
One default to check on your first page: a new page starts public on Free and starts restricted to the list on Pro. If you are on Free, set the page private before you put anything real in it.
The honest limits
If your readers genuinely need to move a control that re-runs your Python, nothing here replaces the app. Keep the app, and use a page for the monthly summary that goes to the people who were never going to open the app anyway.
A published page is a snapshot of what the script produced when you ran it. It is current as of that publish and not a second later, which is fine for weekly numbers and wrong for anything someone might trade on.
The data layer is for form entries and ratings, not for a file upload or a video, and it is not on Free. Free also caps a private page at three named people, so the fifth reader is the moment this becomes a paid tool.
Gradio sits in exactly the same place, for the same reason. Publishing what the interface shows works well. Hosting the running interface is a different product, and you should not pick this one for it.