An opinionated Vite Plus (Vite+) monorepo featuring TanStack Start, Paraglide.js (i18n), Hono, oRPC, drizzle-orm, better-auth, and more.
✨ Live Demo Deployments ✨
Dockerfile (Coolify)
Docker Compose (Coolify)
Merged Web + Server with Dockerfile (Coolify) [see branch]
Merged Web + Server (Cloudflare Workers) [see branch]
- Tech Stack
- Getting Started
- Deployment
- Environment Variables
- Merging Server to Web App
- Issue Watchlist
- Contributing
- Acknowledgements
- License
Here is a non-exhaustive list of the main technologies used in this project, along with their purposes and possible alternatives they replace:
| Technology | Purpose | Replaces/Similar Alternatives |
|---|---|---|
| pnpm | Fast, disk-efficient package manager for Node.js with package catalogs for monorepo dependecy deduplication. | npm, yarn |
| Vite Plus (Vite+) | Unified toolchain for development, testing, and building the monorepo. | Turborepo, Nx, Vitest, Prettier, ESLint, husky, lint-staged, lefthook, tsdown, tsc |
| TanStack Start | Modern full-stack React framework with support for SPA, SSR, ISR, and integrated with TanStack Query. It uses Vite's Nitro adapter for cross-platform deployment portability. | Next.js, Remix, React Router |
| Paraglide.js | Compiled internationalization (i18n) library for managing translations. | i18next, next-intl |
| Hono | Lightweight web server framework built on web standards and is WinterCG-compliant for cross-platform portability. | Express.js, Fastify, Elysia.js |
| oRPC | RPC framework to define API routes and generate OpenAPI specs and documentation with Scalar. | tRPC |
| Drizzle ORM | Type-safe and lightweight ORM for database interactions. | Prisma, TypeORM |
| PostgreSQL | Stable open-source relational database. | MySQL, MariaDB |
| Better Auth | Self-hosted authentication framework with support for all major OAuth providers. | Auth.js |
| Docker | Containerization for local development and deployment. | Podman |
| shadcn/ui | Accessible and customizable React component library. | Chakra UI, Material UI, Mantine UI |
- Node.js ≥ 25
- install via Node.js official website or nvm
- Vite Plus (vp)
- install via
curl -fsSL https://vite.plus | bash(macOS/Linux) orirm https://vite.plus/ps1 | iex(Windows)
- install via
- pnpm ≥ 10
- install via
vp install -g pnpm
- install via
- Docker
- install it via their official website
-
Clone the repository and install dependencies:
git clone https://github.com/tsu-moe/tsu-stack.git cd tsu-stack vp env install # install Node.js version specified in package.json vp install # install all packages in the monorepo
-
Copy the environment files:
cp packages/env/.env.example packages/env/.env
-
Generate a Better-Auth secret and set it as
BETTER_AUTH_SECRETinpackages/env/.env:vp run auth:secret
-
Start the local PostgreSQL database:
vp run db:dev:start # you can stop it later with vp run db:dev:stop -
Migrate the database:
vp run db:migrate
-
Start all development servers:
vp run dev
The following applications will be running:
- Web App:
http://localhost:3000/web - Server:
http://localhost:5000/server
- Web App:
Tip
Run vp run fix to lint, format, and type-check your code. This is also automatically run when you do git commit.
As an alternative to vp run dev, you can run the full stack inside Docker using the local compose file:
cp .env.docker.example .env.docker # And set environment variables as needed
vp run docker:up
vp run docker:up:build # OR: force a rebuild when you make changes to the codeThis project is designed to be deployed as separate applications for the server and web frontend. Below are the recommended deployment strategies and configurations.
Coolify can be used to deploy the server and web applications. Choose a strategy and follow the steps below to configure each app:
Warning
This approach retains rolling updates in Coolify and has minimal downtime, but it is harder to scale compared to Docker Compose.
- Base Directory: Set to
/apps/server. - Domain: Assign a domain, e.g.,
https://example.com/server. - Port: Expose port
5000.
- Base Directory: Set to
/apps/web. - Domain: Assign a domain, e.g.,
https://example.com/web. - Port: Expose port
3000.
Caution
Ensure that the Strip Prefixes option is unchecked in the Advanced settings to avoid issues with custom base paths.
Finally, set any required environment variables in the "Environment Variables" tab for each application and press the "Deploy" button to start the deployment process.
- When creating a new application in Coolify, select "Private Repository (with GitHub App)" and select your repository with your tsu-stack app.
- Next, change the "Build Pack" to "Docker Compose" and set the "Docker Compose Location" to
/docker-compose.coolify.yaml. - Refer to Server Deployment and Web Deployment sections above for domain configurations.
- you need to explicitly bind the port in the domain since Docker Compose doesn't have
Expose Port, for example:https://example.com:3000/webfor the web app andhttps://example.com:5000/serverfor the server.
- Set any required environment variables in the "Environment Variables" tab.
- Press the "Deploy" button to start the deployment process.
Caution
Ensure that the Strip Prefixes option is unchecked in the Advanced settings to avoid issues with custom base paths.
Tip
You can clone the variant/merged-cloudflare branch for a Cloudflare Workers-compatible setup.
You will need to use the @cloudflare/vite-plugin adapter instead of nitro. See this commit for example changes.
Then, you will need to switch to using a singleton db instance instead of a connection pool in the server app since Cloudflare Workers do not support long-lived connections. See this commit for example changes.
You'll need to set the secrets manually in the Cloudflare dashboard before running the deployment commands or via pnpm wrangler secret put <VARIABLE_NAME> one-by-one.
Finally, you can deploy by running the following commands:
pnpm wrangler login # authenticate with your Cloudflare account # copy the shared env variables to the web app's env file for wrangler to read them
vp run deploy # run the deploy task which uses wrangler to deploy the web app to Cloudflare WorkersCaution
I do not recommend this way of deploying because it is prone to human error.
You can take advantage of Cloudflare's Git-based CI/CD by connecting your GitHub repository to your Cloudflare account and configuring the deployment settings to automatically deploy on pushes to your main branch.
You will need to set up the build environment variables and variables and secrets manually in the Cloudflare dashboard in order for builds to succeed.
| Build Settings | Secrets & Variables |
|---|---|
![]() |
![]() |
Note
There isn't an automatic way to do this with my current setup. So you'll need to link the repository first and trigger a failed build, then set the variables and trigger another deployment for the changes to take effect.
TanStack Start uses Nitro as its server engine, which means the web app can be deployed to any platform Nitro supports out of the box - Cloudflare Workers, Vercel, AWS Lambda, Deno Deploy, and many more. Configure the target by setting the appropriate Nitro preset in apps/web/vite.config.ts.
Caution
The server (apps/server) is a Node.js/Hono server and is not compatible with edge runtimes like Cloudflare Workers. It must be deployed to a Node.js-capable environment (e.g. a VPS, container, or serverless platform with Node.js support). This is by design - the dedicated Node.js server is cheaper to self-host for database-heavy workloads.
For the Hono server, use the following environment variables:
| Variable Name | Required | Default Value | Description |
|---|---|---|---|
VITE_SERVER_URL |
✅ | - | Base URL for the server. May also include a subpath if needed, ex: https://example.com/server. |
VITE_WEB_URL |
✅ | - | Base URL for the web app. May also include a subpath if needed, ex: https://example.com/web. |
BETTER_AUTH_SECRET |
✅ | - | Secret key for Better-Auth. Generate with vp run auth:secret. |
DATABASE_URL |
✅ | - | PostgreSQL connection string. |
ENABLE_OPEN_API_DOCS |
❌ | false |
Enable OpenAPI /docs endpoint. |
For the web app, use the following environment variables:
| Variable Name | Required | Default Value | Description |
|---|---|---|---|
VITE_SERVER_URL |
✅ | - | Base URL for the server. May also include a subpath if needed, ex: https://example.com/server. |
VITE_WEB_URL |
✅ | - | Base URL for the web app. May also include a subpath if needed, ex: https://example.com/web. |
BETTER_AUTH_SECRET |
✅ | - | Secret key for Better-Auth. Generate with vp run auth:secret. |
DATABASE_URL |
✅ | - | PostgreSQL connection string. |
VITE_IMGPROXY_URL |
❌ | - | URL for image optimization. You'll need to deploy your own imgproxy container first. |
Tip
An example can be found in the variant/merged branch. You can check this commit to see the changes needed.
Since Hono is built on web standards, you can mount the Hono App into the TanStack Start web server.
// apps/web/package.json
"dependencies": {
// ...
"@tsu-stack/i18n": "workspace:*",
"@tsu-stack/server": "workspace:*", // add this to import the server app into the web app
"@tsu-stack/ui": "workspace:*",
// ...
}Then we can import our app from the server package.
// apps/web/src/routes/server/$.ts
import { createFileRoute } from "@tanstack/react-router";
import { app } from "@tsu-stack/server";
export const Route = createFileRoute("/server/$")({
server: {
handlers: {
GET: ({ request }) => {
return app.fetch(request);
},
POST: ({ request }) => {
return app.fetch(request);
},
},
},
});Then merge your web environment variables with the server ones and make sure VITE_SERVER_URL points to the web domain's subpath.
-VITE_SERVER_URL=http://localhost:5000/server
+VITE_SERVER_URL=http://localhost:3000/web/server
VITE_WEB_URL=http://localhost:3000/webYou will also need to adjust the getConnInfo import to match your runtime environment.
-import { getConnInfo } from '@hono/node-server/conninfo'
+import { getConnInfo } from 'hono/vercel'Warning
hono/vercel works in any environment, but it may not have all the information needed for the logger middleware.
Then lastly, remove the serve() call in apps/server/src/index.ts since the Hono app is now being served by the TanStack Start server.
-import { serve } from "@hono/node-server";
void (async () => {
await migrateDatabase()
- serve(
- {
- fetch: app.fetch,
- port: 5000,
- },
- (info) => {
- logger.info(`Server is running on http://localhost:${info.port}${new URL(apiEnvServer.VITE_SERVER_URL).pathname}`)
- },
- )
})()Warning
You may want to refactor the logging middlewares since the TanStack Start server also logs incoming/outgoing requests, similar to the Hono app's middleware.
Warning
You may also need to adjust your Docker Compose file and the apps/web/Dockerfile to include build args needed in the server app such as DATABASE_URL and handle other environment variables.
When mounting the Hono app into the TanStack Start web server, you will save the resources of running a separate Node.js server.
But keep in mind that all API requests will now consume resources from the web server container, leading to the web server being less responsive under heavy API load.
However, the benefit is singular deployments and lower memory usage for websites that don't receive much traffic (around 70MB with a single app vs 130MB when separated on idle).
Note
I personally keep them separated so that when it's time to scale, I can scale the web server independently or deploy it to another platform like Cloudflare Workers for serverless edge performance.
- Router/Start issues - TanStack Start is in RC.
- Devtools releases - TanStack Devtools is in alpha and may still have breaking changes.
- Nitro v3 - This template is configured with Nitro Nightly (3.0.1-20260128-211656-ae83c97e) by default.
- Currently, when using newer versions of Nitro, you may encounter CJS to ESM interop crashes on build with the error:
TypeError: Cannot destructure property '__extends' of '__toESM$1(...).default' as it is undefined. - This is similar to the issue described in nitrojs/nitro#4113
- Currently, when using newer versions of Nitro, you may encounter CJS to ESM interop crashes on build with the error:
- Better Auth experimental Drizzle adapter - We're using a separate branch of Better Auth's Drizzle adapter that supports Drizzle relations v2.
- Vite+ issues - Vite+ is in alpha.
-
The server and web app must be deployed on the same host using path-based routing (e.g.,
app.example.com/app+app.example.com/server). This usesSameSite=Strictcookies in order to avoid Safari ITP issues. See Better Auth cookie docs for context. -
This implementation does not include security headers by default. You should add the following headers in production for improved security:
Content-Security-PolicyStrict-Transport-SecurityX-Frame-OptionsX-Content-Type-Options: nosniffReferrer-PolicyPermissions-Policy
-
Builds are slower and more bloated in general because Vite Plus does not have a
turbo prunealternative- See this related issue: voidzero-dev/vite-plus#839
-
On a similar note, there isn't an elegant way to install
vpin Dockerfile images, so you need to manually bump the desiredvpversion in the/apps/*/Dockerfile'sVITE_PLUS_VERSIONvariable. -
There is a hydration error when navigating to an i18n subpath like
/de, but it subsides in subsequent navigations.- Need to investigate further, but otherwise, I haven't encountered any app-breaking bugs with it.
-
robots.txtneeds to be at the root of the domain to be detected by search engines (ie.example.com/robots.txt), but since the web app is served on a subpath (ie.example.com/web), you need to set up a redirect fromexample.com/robots.txttoexample.com/web/robots.txtin order for it to be detected.- Other than that, you may need to set up a root sitemap index that links to as many sitemaps for every app you deploy in multiple subpaths.
- At the moment, the
__root.tsxpoints to the subpath-specific sitemap, so you may want to consider pointing it to the root if you decide to opt into that architecture.
- At the moment, the
- Alternatively, you can simply deploy the web app in the root so that the files are hosted in
example.com/robots.txtandexample.com/sitemap.xmlinstead ofexample.com/web/robots.txtandexample.com/web/sitemap.xml.
- Other than that, you may need to set up a root sitemap index that links to as many sitemaps for every app you deploy in multiple subpaths.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. If there are any bugs, please open an issue.
To get started, fork the repo, make your changes, add, commit, and push your changes to your fork. Then, open a pull request. If you're new to GitHub, this tutorial might help.
You can support the project by giving it a star, sharing it with your friends, contributing to the project, and reporting any bugs you find.
This repository builds on mugnavo/tanstarter-plus.
- It will continue to be a reference for new dependency features.
This project is licensed under the MIT License - see the LICENSE file for details.



