TanStack Start
Hetzner.Website.TanStackStart deploys a
TanStack Start app to a Hetzner Cloud
Server. TanStack Start is pure Vite, so Alchemy runs your project’s own
vite build: the SSR server runs as a systemd
Hetzner.Service on port 3000, and
client assets are baked into the unit and served first. Omit server
and Alchemy creates a cpx12 Ubuntu 24.04 box in fsn1. The live URL
is http://{ipv4}:3000 — the Service has no TLS. No adapter.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads its
/tanstack-start and /tanstack-start/node exports from your project
at deploy time. It is only used at build time, so a dev dependency is
enough:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksConfigure Vite
Section titled “Configure Vite”Your vite.config.ts stays what TanStack Start’s scaffold gives
you — no adapter, no deployment preset. Vite plugins like Tailwind
go in plugins as usual:
import tailwindcss from "@tailwindcss/vite";import { tanstackStart } from "@tanstack/react-start/plugin/vite";import viteReact from "@vitejs/plugin-react";import { defineConfig } from "vite";
export default defineConfig({ plugins: [tailwindcss(), tanstackStart(), viteReact()],});Alchemy drives that config’s builder.buildApp() and forces the SSR
bundle to be self-contained, then wraps its fetch handler as a Node
HTTP server on port 3000.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack):
import * as Hetzner from "alchemy/Hetzner";
export const Website = Hetzner.Website.TanStackStart("Website");Pass server to host the site on an existing
Hetzner.Server — several sites and
APIs can share one box. rootDir is the project root — the directory
holding package.json and vite.config.*. It defaults to ".", so
set it only when the app does not sit next to alchemy.run.ts:
export const Website = Hetzner.Website.TanStackStart("Website", { rootDir: "./app",});Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyTanStackSite", { providers: Hetzner.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);Requests matching a built client asset are served first; everything else — SSR routes, server routes, server functions — falls through to the Node handler on the Server.
The live url is http://{ipv4}:3000. site.server and
site.service are the Hetzner resources underneath — undefined
during alchemy dev.
See the TanStack Start API reference for every prop and attribute.
Add environment variables
Section titled “Add environment variables”Process environment is a top-level env map — plain strings, or
Redacted values:
export const Website = Hetzner.Website.TanStackStart("Website", { env: { API_BASE: "https://api.example.com", },});The values are copied onto process.env before the build and the
dev server, and onto the systemd unit at deploy time, so server
code reads the same keys in both modes. Client inlining is whatever
Vite does (VITE_ prefixes).
An output from another resource only exists once that resource is yielded, so declare the site inside the Stack generator instead:
Effect.gen(function* () { const api = yield* Api;
const site = yield* Hetzner.Website.TanStackStart("Website", { env: { API_BASE: api.url }, });
return { url: site.url };});Read the environment in a server function
Section titled “Read the environment in a server function”The TanStack Start server is a plain Node process, so server
functions read the environment from process.env:
import { createServerFn } from "@tanstack/react-start";
const getApiBase = createServerFn({ method: "GET" }).handler(() => ({ apiBase: process.env.API_BASE ?? "unset",}));Values prefixed VITE_ are inlined into the client bundle at build time
instead, as import.meta.env.VITE_* — set those in the shell that runs
alchemy deploy, not in env.
Read the environment in a server route
Section titled “Read the environment in a server route”Server routes run on the same Node process and read process.env the
same way:
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/hello")({ server: { handlers: { GET: () => new Response(process.env.API_BASE ?? "unset"), }, },});Build configuration
Section titled “Build configuration”The build runs through your project’s own vite.config.ts — the
tanstackStart() plugin and everything else in it apply as-is. Keep
vite’s default build.outDir (dist); the integration reads the
server and client output from dist/server and dist/client.
Local dev
Section titled “Local dev”bun alchemy devalchemy dev runs TanStack Start’s own Vite dev server (native HMR)
instead of deploying; site.url is the local address and no Hetzner
resources are created. Wrap the site in Alchemy.remote() to deploy
the live Server and Service even during dev:
export const Website = Hetzner.Website.TanStackStart("Website").pipe( Alchemy.remote(),);Solid variant
Section titled “Solid variant”TanStack Start’s Solid flavor works the same way — swap the plugins in
vite.config.ts and keep the identical alchemy.run.ts:
import { tanstackStart } from "@tanstack/solid-start/plugin/vite";import { defineConfig } from "vite";import viteSolid from "vite-plugin-solid";
export default defineConfig({ plugins: [tanstackStart(), viteSolid({ ssr: true })],});@tanstack/solid-start exposes the same tanstackStart() plugin entry
point and the same client/ssr build environments, so the deploy is
identical.
Custom domain
Section titled “Custom domain”const zone = yield* Hetzner.Zone("dns", { name: "example.com" });const site = yield* Hetzner.Website.TanStackStart("Web", { domain: "app.example.com", zone,});domain requires an existing Hetzner.Zone
— the Website does not create one. It adds an A record pointing at
the Server’s public IPv4. url becomes http://app.example.com:3000.
There is no TLS on the Service.