# Start UI Web bakes in VersityGW, Maildev and a TypeScript 7 editor trap

> Start UI Web is BearStudio's opinionated React starter, scaffolded with pnpm create start-ui -t web and wired to PostgreSQL, an S3-compatible store and a mail catcher. The stack is current and the docs are external, but a few decisions are unusual enough to check before you adopt it: a native TypeScript compiler with no tsserver, a self hosted S3 gateway in the compose file, and a demo mode switch that disables authentication security.

**BearStudio/start-ui-web** — 🚀 Start UI [web] is an opinionated UI starter from the 🐻 BearStudio Team with ⚙️ Node.js, 🟦 TypeScript, ⚛️ React, 📦 TanStack Start, 💨 Tailwind CSS, 🧩 shadcn/ui, 📋 React Hook Form, 🔌 oRPC, 🛠 Prisma, 🔐 Better Auth, 📚 Storybook, 🧪 Vitest, 🎭 Playwright

- Repository: https://github.com/BearStudio/start-ui-web
- Website: https://demo.start-ui.com
- Stars: 1,749 · Forks: 165
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/bearstudio-start-ui-web

## Scaffolding and the four commands that make it run

Start UI is created with a single command rather than by cloning and pruning:

```bash
pnpm create start-ui -t web myApp
```

The -t web flag selects the web starter, and the trailing argument names the folder. Requirements are Node.js 22 or newer, pnpm as the package manager, and either Docker or an existing PostgreSQL database.

Installation after scaffolding is four steps, in this order:

```bash
cp .env.example .env  # Setup your env variables
pnpm install          # Install dependencies
pnpm dk:init          # Start Docker containers (PostgreSQL, S3-compatible storage, Maildev)
pnpm db:init          # Push the Prisma schema and seed the database
```

Then pnpm dk:start if the containers are not already running, followed by pnpm dev. What you should see is the app on port 3000, which VITE_PORT sets, and an API documentation page at http://localhost:3000/api/openapi/app listing the backend routes the oRPC layer exposes.

The no Docker path exists but is narrower than it looks: you supply a PostgreSQL database, replace DATABASE_URL, then run pnpm db:push and pnpm db:seed. Storage and mail are not given a documented non Docker substitute in the README, so a Docker free setup means replacing those services yourself.

## docker-compose.yml runs VersityGW, not MinIO

The compose file defines four services and the storage one is the notable choice. The s3 service runs ghcr.io/versity/versitygw:v1.8.0 with VGW_BACKEND set to posix, storing objects under /data/s3 on a named volume. Ports 7070 for the API and 7071 for the web console are published, with environment overrides DOCKER_S3_PORT and DOCKER_S3_UI_PORT.

Several settings exist specifically to make local browser uploads work, and they are worth understanding before you copy the pattern. VGW_WEBUI_GATEWAYS is pinned to http://localhost:${DOCKER_S3_PORT:-7070} because the console runs in the browser on the host and cannot resolve a compose service name. VGW_CORS_ALLOW_ORIGIN is set to '*' to allow presigned PUT uploads from any local origin, which the file itself labels dev only. VGW_REGION is tied to S3_REGION with a default of auto because the region has to match what the application uses to sign requests, or signatures fail.

The other services are postgres:16.1 with a pg_isready healthcheck, and maildev 2.2.1 on SMTP 1025 and UI 1080 with a /healthz probe. A fourth service, s3-init, runs the amazon/aws-cli image under an init profile, so it exists to populate buckets rather than to serve traffic.

Both database and object storage containers take their credentials from .env through env_file, so the same secrets the application reads also configure the services. The compose stack defines a network named start-ui-web_default, and the README's docker run example joins it by that exact name to resolve postgres, s3 and maildev as hostnames.

## Build-time VITE_ variables versus runtime secrets

The Dockerfile draws a line that matters for container security, and the README states it explicitly. Only the VITE_* variables are build arguments, because they are inlined into the client bundle and are therefore public by construction. DATABASE_URL, AUTH_SECRET, EMAIL_* and S3_* are deliberately not build args: they are read at runtime from the container environment so they never end up in image layers.

The build command shows the client side of that split:

```bash
docker build -t start-ui-web \
  --build-arg VITE_BASE_URL="http://localhost:3000" \
  --build-arg VITE_S3_BUCKET_PUBLIC_URL="http://localhost:7070/default" \
  --build-arg VITE_ENV_NAME="DOCKER" \
  --build-arg VITE_ENV_COLOR="blue" \
  .
```

The consequence for deployment is that the same image is not environment agnostic. VITE_BASE_URL and VITE_S3_BUCKET_PUBLIC_URL are baked in at build time, so two environments with different public URLs need two builds. The runtime side passes server values with -e flags or an --env-file.

The base image is the official pnpm image with the Node runtime pinned by a NODE_VERSION build argument defaulting to 24, set through pnpm runtime set node so a single value serves every stage. Dependency installation uses --ignore-scripts so the postinstall and lefthook hooks do not run inside the image build, then prisma generate runs explicitly. The final container runs as the unprivileged node user, listens on port 3000 and exposes a HEALTHCHECK.

## TypeScript 7 with no tsserver, and what breaks in your editor

This is the most likely first hour of friction. The project uses TypeScript 7, the native compiler. It ships a tsc binary but no tsserver, so an editor cannot typecheck the way the command line does unless it is running the native language server. Without that, VS Code falls back to its bundled TypeScript 5 and can report different errors than CI produces.

The fix is the TypeScriptTeam.native-preview extension, and the example editor settings already enable it:

```jsonc
"js/ts.experimental.useTsgo": true,
"js/ts.tsdk.path": "./node_modules/typescript"
```

Those settings come from copying .vscode/settings.example.json to .vscode/settings.json, or .zed/settings.example.json to .zed/settings.json for Zed. Since the repository documents TypeScript 7 as TypeScriptTeam.native-preview, a team standardising on stable TypeScript 5 tooling will find the lint script lint:ts, which runs tsc --noEmit, behaving differently from the editor unless the extension is present.

Code quality tooling is oxlint with --fix plus oxfmt for formatting, configured through .oxlintrc.json and .oxfmtrc.json, and git hooks come from lefthook.yml installed by the prepare script. Static analysis is configured through .sonarcloud.properties, and AGENTS.md and .claude/ sit at the repository root, which says something about the project's ambitions for agent driven contribution workflows.

## Email is caught, not sent, and templates have a preview server

Nothing leaves your machine in development. Maildev receives the messages on port 1025 and its web interface on localhost:1080 shows them, with the UI port overridable through DOCKER_MAILDEV_UI_PORT in .env.

Templates are React email components under src/emails, built with the email dev server on its own port:

```bash
pnpm dev:email
```

That preview runs on port 3030 or the next free one, deliberately separate from the Vite app on 3000. Templates live under src/emails/templates, and each should export PreviewProps with realistic fixtures, including language, which has no default injected, and an OTP code. The preview toolbar's Props tab lets you switch language live without editing the template.

One wrinkle catches Windows users: email images live in public/static, served by the app at ${VITE_BASE_URL}/static, and src/emails/templates/static is a symlink to that directory so the preview server can find them too. Git does not create symlinks on Windows by default, so core.symlinks has to be enabled before cloning.

## VITE_IS_DEMO is the switch to leave alone

The environment file documents a demo mode that is easy to enable by accident. VITE_IS_DEMO defaults to false, and the comment above it is a warning rather than a description: demo mode disables authentication security, mocking the OTP as 000000 and seeding accounts. The stated use is a public demo with disposable seed data, and it says never to enable it on a deployment with real users.

That is the single most dangerous configuration line in a starter, because the whole point of the stack is that auth works. A copy pasted .env that flips this to true in a staging environment produces an application where a six digit code is known in advance.

The rest of the auth configuration is more conventional. AUTH_SECRET ships as the placeholder REPLACE ME, with a suggested generator, npx @better-auth/cli@latest secret. AUTH_SESSION_EXPIRATION_IN_SECONDS is 2592000, thirty days, with AUTH_SESSION_UPDATE_AGE_IN_SECONDS at 86400 so the expiration is refreshed daily. AUTH_ALLOWED_HOSTS is commented out with an example for vercel.app subdomains, and AUTH_TRUSTED_ORIGINS lists the start-ui-native:// mobile scheme, which shows a native app is an intended target alongside the browser.

## What a starter cannot tell you

Three things are missing that you will look for. There are no GitHub releases, so upgrading is following the main branch rather than bumping a tag, even though package.json carries a version of 3.0.0. Detailed documentation lives off repository at docs.web.start-ui.com, which means the guide and the code can disagree at any given moment. And package.json is marked private.

The alternative to a starter like this is assembling the same stack yourself: TanStack Start for the full stack React framework, Tailwind and shadcn/ui for the interface layer, Prisma for data access, Better Auth for identity, oRPC for the typed API boundary. That route gives you every component at a version you choose and no conventions imposed, at the cost of wiring the four integration seams that this repository has already solved, particularly the distinction between build time client variables and runtime server secrets and the local object storage setup that a browser upload needs. The starter's real value is those seams, not the list of dependencies in its README.

## Conclusion

Start UI Web fits a team that wants a full stack product skeleton in an afternoon and is willing to keep its conventions, including the S3 gateway in docker-compose.yml and the native TypeScript toolchain. It does not fit a team on npm rather than pnpm, one without Docker or a PostgreSQL instance, or one that needs Docker absent entirely since three of the four local services are containers. Verify first that your editor runs the TypeScript 7 language server, because without the extension VS Code falls back to TypeScript 5 and reports different errors than pnpm lint:ts, and leave VITE_IS_DEMO at false outside a disposable public demo.

## FAQ

### How do I create a new Start UI Web project?

Run pnpm create start-ui -t web myApp, which scaffolds a folder with the latest version. You need Node.js 22 or newer, pnpm, and Docker or a PostgreSQL database.

### Which object storage does Start UI Web use in development?

The docker-compose.yml runs VersityGW (ghcr.io/versity/versitygw:v1.8.0) with a posix backend on ports 7070 and 7071. Its region must match the S3_REGION the app signs requests with, and CORS is opened to any origin for local browser uploads.

### Why does my editor report different TypeScript errors than CI?

The project uses TypeScript 7, which ships a tsc binary but no tsserver. Install the TypeScriptTeam.native-preview extension and enable useTsgo in the example editor settings, otherwise the editor falls back to TypeScript 5.

### How do I see emails sent by the app during development?

Maildev catches them instead of sending. Its interface is at localhost:1080, and the react-email preview server for templates runs through pnpm dev:email on port 3030.

### What does VITE_IS_DEMO do?

It enables a read only demo mode that disables authentication security by mocking the OTP as 000000 and seeding accounts. The .env.example comment says to enable it only on a public demo with disposable data, never with real users.

### Which environment variables are needed at Docker build time?

Only the VITE_* ones, since they are inlined into the client bundle. Server values such as DATABASE_URL, AUTH_SECRET, EMAIL_* and S3_* are passed at container run time so they never land in image layers.

## Sources

- [BearStudio/start-ui-web on GitHub](https://github.com/BearStudio/start-ui-web)
- [Issues](https://github.com/BearStudio/start-ui-web/issues)
- [License: MIT](https://github.com/BearStudio/start-ui-web/blob/main/LICENSE)
- [Project website](https://demo.start-ui.com)
- [README](https://github.com/BearStudio/start-ui-web/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bearstudio-start-ui-web
