Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
001df8f
feat(web): migrate from Next.js to TanStack Start
adriangohjw May 17, 2026
352c228
chore: loosen tanstack-start catalog version pinning and update lockfile
adriangohjw May 17, 2026
b1eeddc
build(web): upgrade vite 7 → 8 with Rolldown-compatible fixes
adriangohjw May 17, 2026
8438cd7
style: format
adriangohjw May 17, 2026
b2c07af
fix: lint
adriangohjw May 17, 2026
12616b3
chore: update tsconfig.json to exclude additional directories
adriangohjw May 17, 2026
6cbb1a1
chore: update dependencies and configuration for Babel and Vite
adriangohjw May 17, 2026
659b8df
feat(web): migrate env vars and configs from Next.js to TanStack Star…
adriangohjw May 17, 2026
880499c
chore(web): replace vite-tsconfig-paths plugin with native Vite resol…
adriangohjw May 17, 2026
1253a64
chore(web): replace vite-tsconfig-paths in vitest config with native …
adriangohjw May 17, 2026
09b93ad
fix(web): replace __dirname with ESM-compatible import.meta.url
adriangohjw May 17, 2026
7a892a1
chore(web): stage remaining migration changes
adriangohjw May 17, 2026
301e460
chore(web): clean up Next.js references in configs and instrumentation
adriangohjw May 17, 2026
88105c0
fix(web): correct broken import in email validator
adriangohjw May 17, 2026
875cb26
fix(web): use default import for CJS email-addresses package
adriangohjw May 17, 2026
c17ea9c
fix(web): use default import for CJS email-addresses in user service
adriangohjw May 17, 2026
3d1b272
chore(web): exclude routeTree.gen.ts from linter and formatter
adriangohjw May 17, 2026
eecd285
chore(web): adapt Storybook preview for TanStack Router
adriangohjw May 17, 2026
3607ad0
chore(web): remove Next.js 'use client' directives and stale RSC comment
adriangohjw May 17, 2026
91a01ee
chore(web): delete dead RSC-era tRPC files
adriangohjw May 17, 2026
d6a73b1
feat(web): migrate <img> to @unpic/react <Image>
adriangohjw May 17, 2026
8ff3818
chore(web): remove migration dead code and wire up tracer + CSP
adriangohjw May 17, 2026
5404ce9
chore(web): regenerate routeTree for TanStack Router setup
adriangohjw May 17, 2026
fc6d82d
feat(web): add router progress bar
adriangohjw May 17, 2026
a2b38e6
chore(web): cap vite server request body size to 2 MB
adriangohjw May 17, 2026
a2f63bb
docs(web): clarify CSP unsafe-inline tradeoff and router options comment
adriangohjw May 17, 2026
25fb884
fix(web): remove invalid maxRequestBodySize from tanstackStart config
adriangohjw May 17, 2026
ebfa506
feat(web): enforce 2 MB request body limit via middleware
adriangohjw May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SESSION_SECRET='please-generate-a-secret-and-put-it-here'

# Change this to your app name. This is used when the application needs to refer to itself,
# e.g. in emails or on the landing page.
NEXT_PUBLIC_APP_NAME='Starter Kit'
VITE_APP_NAME='Starter Kit'

# Sets the debug level for logging. Can be one of: silent, error, warn, info, debug, trace
LOG_LEVEL="debug"
5 changes: 0 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ updates:
patterns:
- 'prisma'
- '@prisma/*'
next:
applies-to: version-updates
patterns:
- '@next/*'
- 'next'
react:
applies-to: version-updates
patterns:
Expand Down
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ node_modules
coverage
**/test-results

# next.js
.next/
out/
next-env.d.ts

# production
build

Expand Down
14 changes: 7 additions & 7 deletions .oxfmtrc.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["turbo/generators/templates/**/*.hbs"],
"ignorePatterns": ["turbo/generators/templates/**/*.hbs", "apps/web/src/routeTree.gen.ts"],
"printWidth": 80,
"semi": false,
"singleQuote": true,
Expand Down Expand Up @@ -38,13 +38,13 @@
"modifiers": ["value"],
},
{
"elementNamePattern": ["next", "next/*"],
"groupName": "next-types",
"elementNamePattern": ["@tanstack/react-start", "@tanstack/react-start/*", "@tanstack/react-router", "@tanstack/react-router/*"],
"groupName": "tanstack-types",
"selector": "type",
},
{
"elementNamePattern": ["next", "next/*"],
"groupName": "next-values",
"elementNamePattern": ["@tanstack/react-start", "@tanstack/react-start/*", "@tanstack/react-router", "@tanstack/react-router/*"],
"groupName": "tanstack-values",
"selector": "external",
"modifiers": ["value"],
},
Expand All @@ -63,8 +63,8 @@
"groups": [
"react-types",
"react-values",
"next-types",
"next-values",
"tanstack-types",
"tanstack-values",
"builtin",
"external",
"acme-types",
Expand Down
22 changes: 10 additions & 12 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,40 @@
"editor.formatOnSave": true,
"editor.formatOnPaste": false,
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "always"
"source.fixAll.oxc": "always",
},
"oxc.typeAware": true,
"tailwindCSS.classFunctions": ["cva", "cx", "cn"],
"tailwindCSS.experimental.classRegex": [
":\\s*?[\"'`]([^\"'`]*).*?,",
[
"classNames:\\s*{([\\s\\S]*?)}",
"\\s?[\\w].*:\\s*?[\"'`]([^\"'`]*).*?,?\\s?"
"\\s?[\\w].*:\\s*?[\"'`]([^\"'`]*).*?,?\\s?",
],
["classNames={{([^}]*)}}", "[\"'`]([^\"'`]*)[\"'`]"],
[
"tv\\(([^)(]*(?:\\([^)(]*(?:\\([^)(]*(?:\\([^)(]*\\)[^)(]*)*\\)[^)(]*)*\\)[^)(]*)*)\\)",
"\"(.*?)\""
"\"(.*?)\"",
],
["class:\\s*?[\"'`]([^\"'`]*).*?,"]
["class:\\s*?[\"'`]([^\"'`]*).*?,"],
],
"files.associations": {
"**/tooling/typescript/*.json": "jsonc",
"**/tsconfig.json": "jsonc",
"**/tsconfig.*.json": "jsonc",
"*.css": "tailwindcss"
"*.css": "tailwindcss",
},
"typescript.preferences.autoImportFileExcludePatterns": [
"zod/dist/types/index.d.ts", // Force auto-imports to v4
"next/router.d.ts",
"next/dist/client/router.d.ts",
"tailwind-variants/dist/index.d.ts" // Force imports from oui-theme instead
"tailwind-variants/dist/index.d.ts", // Force imports from oui-theme instead
],
"json.schemas": [
{
"fileMatch": ["task-definition.json"],
"url": "https://ecs-intellisense.s3-us-west-2.amazonaws.com/task-definition/schema.json"
}
"url": "https://ecs-intellisense.s3-us-west-2.amazonaws.com/task-definition/schema.json",
},
],
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
"editor.defaultFormatter": "Prisma.prisma",
},
}
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A technical kit to quickly build new products from

- 🗼 Monorepo setup with [Turborepo](https://turborepo.com)
- 🧙‍♂️ E2E typesafety with [tRPC v11](https://trpc.io)
- ⚡ Full-stack React with [Next.js v15](https://nextjs.org)
- ⚡ Full-stack React with [TanStack Start](https://tanstack.com/start)
- 🌈 Database with [Prisma](https://www.prisma.io/)
- ⚙️ VSCode extensions
- 🎨 Oxlint + Oxfmt
Expand All @@ -33,7 +33,7 @@ A technical kit to quickly build new products from
└─ Oxfmt formatter configuration
apps
└─ web
├─ Next.js 15
├─ TanStack Start
├─ React 19
├─ Tailwind CSS v4
└─ E2E Typesafe API Server & Client
Expand Down Expand Up @@ -112,9 +112,9 @@ If not set, OTP emails will be logged to the console instead.

#### Retrieving client-side environment variables in code

⚠️ When adding client-only environment variables in NextJS, you must prefix the variable with `NEXT_PUBLIC_` to ensure that the variable is exposed to the browser. For example, if you want to add a variable called `MY_ENV_VAR`, you should add it to your `.env` file as `NEXT_PUBLIC_MY_ENV_VAR`.
⚠️ When adding client-only environment variables, you must prefix the variable with `VITE_` to ensure that the variable is exposed to the browser. For example, if you want to add a variable called `MY_ENV_VAR`, you should add it to your `.env` file as `VITE_MY_ENV_VAR`.

You will also need to update the various environment files (like [apps/web/src/env.ts](apps/web/src/env.ts#L5) or [packages/db/src/env.ts](packages/db/src/env.ts#L6)) to explicitly reference the variable so NextJS will correctly bundle the environment variable into the client-side bundle.
You will also need to update the various environment files (like [apps/web/src/env.ts](apps/web/src/env.ts) or [packages/db/src/env.ts](packages/db/src/env.ts)) to explicitly reference the variable so it is correctly bundled into the client-side bundle.

### 2. Adding a new package

Expand All @@ -141,9 +141,9 @@ and tutorials, can be found [here](https://start.open.gov.sg).

### Deploy to Vercel

Let's deploy the Next.js application to [Vercel](https://vercel.com). If you've never deployed a Turborepo app there, don't worry, the steps are quite straightforward. You can also read [the official Turborepo guide](https://vercel.com/docs/concepts/monorepos/turborepo) on deploying to Vercel.
Let's deploy the TanStack Start application to [Vercel](https://vercel.com). If you've never deployed a Turborepo app there, don't worry, the steps are quite straightforward. You can also read [the official Turborepo guide](https://vercel.com/docs/concepts/monorepos/turborepo) on deploying to Vercel.

1. Create a new project on Vercel, select the `apps/web` folder as the root directory. Vercel's zero-config system should handle all configurations for you.
1. Create a new project on Vercel, select the `apps/web` folder as the root directory. Set the build command to `pnpm vercel-build` and the output directory to `.output`.

2. Add the prerequisite environment variables outlined in the [Prerequisites](#prerequisites) section.

Expand Down
2 changes: 1 addition & 1 deletion apps/web/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"typeAware": true,
"reportUnusedDisableDirectives": "off"
},
"ignorePatterns": ["*.config.*"],
"ignorePatterns": ["*.config.*", "src/routeTree.gen.ts"],
"overrides": [
{
"files": ["**/*.{js,ts,tsx}"],
Expand Down
15 changes: 13 additions & 2 deletions apps/web/.storybook/decorators.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { useState } from 'react'

import type { Decorator } from '@storybook/nextjs-vite'
import {
createMemoryHistory,
createRootRoute,
createRouter,
} from '@tanstack/react-router'

import type { Decorator } from '@storybook/react-vite'
import {
defaultShouldDehydrateQuery,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { createTRPCClient, httpLink, loggerLink } from '@trpc/client'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { NuqsAdapter } from 'nuqs/adapters/react'
import SuperJSON from 'superjson'

import type { AppRouter } from '~/server/api/root'
import { TRPCProvider } from '~/trpc/react'

export const storybookRouter = createRouter({
routeTree: createRootRoute().addChildren([]),
history: createMemoryHistory({ initialEntries: ['/'] }),
})

/**
* This decorator wraps stories with TRPC and React Query and the other app-level providers.
* It mimics the setup in `apps/web/src/app/provider.tsx` and `apps/web/src/app/layout.tsx`.
Expand Down
28 changes: 20 additions & 8 deletions apps/web/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRequire } from 'node:module'
import { dirname, join } from 'path'

import type { StorybookConfig } from '@storybook/nextjs-vite'
import type { StorybookConfig } from '@storybook/react-vite'

const require = createRequire(import.meta.url)

Expand All @@ -13,25 +13,37 @@ function getAbsolutePath(value: string): string {
return dirname(require.resolve(join(value, 'package.json')))
}
const config: StorybookConfig = {
features: {
experimentalRSC: true,
},
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
getAbsolutePath('@storybook/addon-docs'),
getAbsolutePath('@storybook/addon-a11y'),
getAbsolutePath('@storybook/addon-vitest'),
],
framework: {
name: getAbsolutePath('@storybook/nextjs-vite'),
name: getAbsolutePath('@storybook/react-vite'),
options: {},
},
async viteFinal(config) {
// TailwindCSS is not being imported properly within Storybook, so we add the
// TailwindCSS plugin to Vite directly here.
const { default: tailwindcss } = await import('@tailwindcss/vite')
const { mergeConfig } = await import('vite')
return mergeConfig(config, { plugins: [tailwindcss()] })
// TanStack Start plugins (pulled from vite.config.ts) conflict with
// Storybook's multi-entry build under Rolldown (Vite 8). Storybook stories
// only need React component context, not TanStack Start routing.
const filteredConfig = {
...config,
plugins: (config.plugins ?? [])
.flat(Infinity as 1)
.filter(
(p: unknown) =>
!p || !(p as { name?: string }).name?.startsWith('tanstack')
),
}
return mergeConfig(filteredConfig, {
plugins: [tailwindcss()],
define: {
'process.env': {},
},
})
},
staticDirs: ['../public'],
}
Expand Down
18 changes: 10 additions & 8 deletions apps/web/.storybook/preview.ts → apps/web/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Preview } from '@storybook/nextjs-vite'
import { RouterContextProvider } from '@tanstack/react-router'

import type { Preview } from '@storybook/react-vite'
import { initialize, mswLoader } from 'msw-storybook-addon'

import { viewport } from '@acme/storybook-config'

import { ibmPlexMono, inter } from '~/lib/fonts'
import { storybookRouter } from './decorators'

import '../src/app/globals.css'

// Initialize MSW
Expand All @@ -13,9 +16,6 @@ initialize({

const preview: Preview = {
parameters: {
nextjs: {
appDirectory: true,
},
layout: 'fullscreen',
viewport,
chromatic: {
Expand All @@ -32,13 +32,15 @@ const preview: Preview = {
(Story) => {
// Apply the same font classes as the main app
document.documentElement.classList.add(
ibmPlexMono.variable,
inter.variable,
'text-base-content-default',
'font-sans',
'antialiased'
)
return Story()
return (
<RouterContextProvider router={storybookRouter}>
<Story />
</RouterContextProvider>
)
},
],
loaders: [mswLoader],
Expand Down
37 changes: 0 additions & 37 deletions apps/web/next.config.js

This file was deleted.

Loading
Loading