Skip to content

tsu-moe/tsu-stack

Repository files navigation

NodeJS version badge License badge Last commit badge GitHub stars badge GitHub forks badge GitHub issues badge GitHub pull requests badge Repo views badge

An opinionated Vite Plus (Vite+) monorepo featuring TanStack Start, Paraglide.js (i18n), Hono, oRPC, drizzle-orm, better-auth, and more.

tsu!stack Screenshot

✨ Live Demo Deployments ✨
Dockerfile (Coolify)
Docker Compose (Coolify)
Merged Web + Server with Dockerfile (Coolify) [see branch]
Merged Web + Server (Cloudflare Workers) [see branch]

Table of Contents

Tech Stack

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

Getting Started

Prerequisites

  • Node.js ≥ 25
  • Vite Plus (vp)
    • install via curl -fsSL https://vite.plus | bash (macOS/Linux) or irm https://vite.plus/ps1 | iex (Windows)
  • pnpm ≥ 10
    • install via vp install -g pnpm
  • Docker

Setup

  1. 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
  2. Copy the environment files:

    cp packages/env/.env.example packages/env/.env
  3. Generate a Better-Auth secret and set it as BETTER_AUTH_SECRET in packages/env/.env:

    vp run auth:secret
  4. Start the local PostgreSQL database:

    vp run db:dev:start
    # you can stop it later with vp run db:dev:stop
  5. Migrate the database:

    vp run db:migrate
  6. Start all development servers:

    vp run dev

    The following applications will be running:

    • Web App: http://localhost:3000/web
    • Server: http://localhost:5000/server

Tip

Run vp run fix to lint, format, and type-check your code. This is also automatically run when you do git commit.

Running with Docker Locally

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 code

Deployment

This project is designed to be deployed as separate applications for the server and web frontend. Below are the recommended deployment strategies and configurations.

Coolify

Coolify can be used to deploy the server and web applications. Choose a strategy and follow the steps below to configure each app:

Option 1: Separate Dockerfiles

Warning

This approach retains rolling updates in Coolify and has minimal downtime, but it is harder to scale compared to Docker Compose.

Server Deployment
  1. Base Directory: Set to /apps/server.
  2. Domain: Assign a domain, e.g., https://example.com/server.
  3. Port: Expose port 5000.
Web Deployment
  1. Base Directory: Set to /apps/web.
  2. Domain: Assign a domain, e.g., https://example.com/web.
  3. 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.

Option 2: Docker Compose

  1. When creating a new application in Coolify, select "Private Repository (with GitHub App)" and select your repository with your tsu-stack app.
  2. Next, change the "Build Pack" to "Docker Compose" and set the "Docker Compose Location" to /docker-compose.coolify.yaml.
  3. 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/web for the web app and https://example.com:5000/server for the server.
  1. Set any required environment variables in the "Environment Variables" tab.
  2. 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.

Cloudflare Workers

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 Workers

Caution

I do not recommend this way of deploying because it is prone to human error.

Git-based CI/CD

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
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.

Deploying to Other Platforms

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.

Environment Variables

Server

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.

Web

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.

Merging Server to Web App

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/web

You 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.

Resource Usage

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.

Issue Watchlist

  • 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
  • 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.

Pitfalls

  • 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 uses SameSite=Strict cookies 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-Policy
    • Strict-Transport-Security
    • X-Frame-Options
    • X-Content-Type-Options: nosniff
    • Referrer-Policy
    • Permissions-Policy
  • Builds are slower and more bloated in general because Vite Plus does not have a turbo prune alternative

  • On a similar note, there isn't an elegant way to install vp in Dockerfile images, so you need to manually bump the desired vp version in the /apps/*/Dockerfile's VITE_PLUS_VERSION variable.

  • 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.txt needs 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 from example.com/robots.txt to example.com/web/robots.txt in 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.tsx points to the subpath-specific sitemap, so you may want to consider pointing it to the root if you decide to opt into that architecture.
    • Alternatively, you can simply deploy the web app in the root so that the files are hosted in example.com/robots.txt and example.com/sitemap.xml instead of example.com/web/robots.txt and example.com/web/sitemap.xml.

Contributing

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.

Acknowledgements

This repository builds on mugnavo/tanstarter-plus.

  • It will continue to be a reference for new dependency features.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Vite Plus (Vite+) TanStack Start monorepo with Paraglide.js (i18n), Hono, oRPC, drizzle-orm, better-auth, and more. Dockerized and opinionated.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

Contributors