Share a SvelteKit app in dev, in preview and built
vite dev shares as it is, form actions included. vite preview and the Node build refuse the link's forms until they're told its origin.
Updated
In development, a SvelteKit app shares with nothing to change: the page, hot reload and form actions all work through a ouicu link. Built, in vite preview or on Node, SvelteKit checks where each form comes from and refuses the link's, until you name its origin or let the server read ouicu's headers.
| SvelteKit | |
|---|---|
| Dev server port | 5173 |
| On your Wi-Fi | npm run dev -- --host |
| Host check | server.allowedHosts |
| Build folder to upload | build, from npm run build |
| Through the link | What it takes |
|---|---|
| The page | Works, nothing to change |
| Hot reload | Works, nothing to change |
| A form action, in vite dev | Works, nothing to change |
Share vite dev with ouicu
npm run dev# in a second terminalouicu share 5173SvelteKit runs on Vite, whose host check would refuse a tunnel that passes your public name on; ouicu hands it Host: localhost:5173, so it passes (share a Vite dev server has the details). Save a .svelte file and the change shows in place on every screen with the link open.
Form actions post too, for a plain reason: in development SvelteKit skips its CSRF check. We sent one from another site's origin straight to the dev server, and it went through. That is also why a form that worked all week can fail the first time you build. On Free, a share runs for up to 2 hours.
Cross-site POST form submissions are forbidden
Build the app and serve it with vite preview, share that port, and a form action is refused with 403. SvelteKit compares the form's Origin, the link's, with its own, which it takes from the request: Host is localhost, so the two never match. Sent straight to the server, the answer reads:
Cross-site POST form submissions are forbiddenThrough a ouicu link the visitor reads the same words, Cross-site POST form submissions are forbidden, with the same 403. SvelteKit sends them with no content type, and ouicu passes a short error like that on as plain text, so you can read why the form was refused. The SvelteKit server from adapter-node answers the same way.
Name the link in csrf.trustedOrigins
SvelteKit 3 has no svelte.config.js; its migration guide says: “Instead of declaring project configuration in svelte.config.js, it must now be passed to the sveltekit Vite plugin in vite.config.js.” The check can't be turned off either: “CSRF protection is always on; instead of disabling it with checkOrigin: false, allow trusted cross-origin hosts with csrf.trustedOrigins.” So in vite.config.ts:
sveltekit({ adapter: adapter(), csrf: { trustedOrigins: ['https://calm-otter-4821.ouicu.app'] },}),Build again, and the form posts through the link. Write it as the docs ask: “Each origin should be a complete origin including protocol (e.g., https://payment-gateway.com).” A wildcard doesn't work: with https://*.ouicu.app the form was still refused, as SvelteKit matches whole origins. So this suits a reserved name, on Hobby and Pro; a random name means a new line, and a new build, each time. And mind the warning:
“Only add origins you completely trust, as this bypasses CSRF protection for those origins.”
On Node: read ouicu's headers instead
The Node adapter's docs start from the same problem: “HTTP doesn't give SvelteKit a reliable way to know the URL that is currently being requested.” Its answer works for any name, so it suits a share better. ouicu sends the public address in X-Forwarded-Host and X-Forwarded-Proto; tell the server to read them:
npm run buildPROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build# in a second terminalouicu share 3000The adapter's docs give PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build for this. With it, the form posted through the link. One thing that no longer works: ORIGIN. In adapter-node 6.0.0 setting it changed nothing; the origin now comes from paths.origin, fixed when you build:
“
paths.originreplacesprerender.origin, and should reflect your app's public-facing origin if it can't reliably be derived from request headers (for example because it's behind a reverse proxy).”
Built with paths: { origin: 'https://…' } set to the link, the form posted as well: one link per build, as with trustedOrigins.
Upload a static build
A site that is prerendered whole uploads as plain files, and stays up with your laptop closed. With @sveltejs/adapter-static and export const prerender = true in the root layout, the build lands in build:
npm run buildouicu deploy buildWe left out the form route for that build: an upload has no server to run its action. More in Uploading a built site.
Phones, and who can open it
ouicu prints the link as a QR code for your phone (QR codes). On the same Wi-Fi without a link, start Vite with npm run dev -- --host, as in how to open localhost on your phone. A link reaches every route, form actions included, so for a client ask for a password, on Hobby and Pro: ouicu share 5173 --password.
Tested with SvelteKit 3.0.1 on , with Node 24.21.0: the minimal starter from sv create, with Svelte 5.57.2 and a form action, shared with ouicu share through ouicu's edge and opened in Chromium at its https link: vite dev with an edit, vite preview before and after trustedOrigins, adapter-node with its header variables, ORIGIN and paths.origin, and adapter-static.
Sources
Prices, defaults and quotes about other products, and the day each was last checked at its source.
- Server Options, Vite docs. Checked .
- Static site generation, SvelteKit docs. Checked .
- runtime/server/respond.js, SvelteKit on GitHub. Checked .
- Migrating to SvelteKit v3, SvelteKit docs. Checked .
- @sveltejs/kit/vite, SvelteKit docs. Checked .
- Node servers, SvelteKit docs. Checked .