A sophisticated and opinionated boiler-plate built for simplicity and readiness.
Carlo's starter for making a Vike + Solid app with batteries included on stuff I like after experimenting for years.
This is handcrafted from my own research and experience. My goal for this is almost like Rails where opinions > flexibility. This might not work for you, but it works for me. π€
You can also try my other starters:
- π Solid Hop - Less-opinionated Vike Solid boilerplate. Like
npx create solid
but simpler. - π§‘ Svelte Launch - Svelte, but same robust practices.
- π Handcrafted and minimal - picked and chose "do one thing, do it well" libraries that are just enough to get the job done. Just looks a bit bloated at a glance. (I kinda made my own NextJS from scatch here). But it's a minimal Rails-like experience that won't need you to sign up for 5 different unnecessary third-party services just because you can't waste time building your own. I spent hours handcrafting it so you won't have to.
- β‘οΈ Super-fast dev server - way faster than NextJS thanks to Vite. You need to feel it to believe it! It can also literally build your app in seconds.
- π¦ Type-safe Routing - Inspired by TanStack Router, I'm the author of
vike-routegen
which codegens typesafe page routing for you, and it's a breeze! - π¨ Fast, efficient, fine-grained Reactivity - thanks to Solid, it's possibly the most enjoyable framework I used that uses JSX. Has state management primitives out-of-the-box and keeps the experience a breeze.
- π Extremely customizable - you're not at the mercy of limited APIs and paradigms set by big frameworks or third-party services. Swap with your preferred JS backend framework/runtime if you want. Vike is just a middleware. Most of the tech I use here are open-source and roll-your-own type of thing. Hack it up! You're a dev aren't you?
- βοΈ Selfhost-ready - Crafted with simple hosting in mind that'll still probably scale to millions. Just spin up Docker container on a good'ol VPS without locking into serverless. DHH and Shayan influenced me on this. You can still host it on serverless tho. I think? lol
- π Batteries-included - took care of the hard stuff for you. A well-thought-out folder structure from years of making projects: a design system, components, utilities, hooks, constants, an adequate backend DDD-inspired sliced architecture that isn't overkill, dockerizing your app, and most importantly---perfectly-crafted those pesky config files.
- π₯ Robust Error Practices - I thoughtfully made sure there's a best practice for errors here already. You can throw errors in a consistent manner in the backend and display them consistently in the frontend.
- π Documented - OpenAPI docs + Scalar. Aside from that, you'll find most my practices well documented here. It's an accumulation of my dev experience.
- π Authentication-Ready - One thing devs get stuck on. There's a practical auth implemented from scratch here that doesn't vendor-lock you into any auth provider.
- Email & Password
- Transactional Emails (Forgot Password, Email Verification)
- OAuth (Google, GitHub, extend as you wish) w/ linking
- Magic Link
- OTPs
- 2FA
- Organization Auth (easily opt-outable)
- User Management Dashboard
- Rate Limits + Global Rate Limits
- Bun - Runtime and package manager. You can always switch to Node and PNPM if you wish.
- SolidJS - Frontend framework that I like. Pretty underrated, but devx is superior than any other framework I tried!
- Vike - Like NextJS, but just a middleware. SSR library on-top of Vite. Use on any JS backend. Flexible, Simple, and Fast!
- Hono - 2nd fastest Bun framework(?), run anywhere, uses easy-to-understand web-standard paradigms w/ typesafety and a bunch of QoLs built-in.
- OpenAPI - A standard doc that other clients can use for your API (i.e. on Flutter, etc.) w/ hono-openapi.
- Tailwind - Styling the web has been pretty pleasant with it. I even use it on React Native for work. It's amazing.
- Tanstack Form & Tanstack Query - No need to rebuild validation, caching, retries, etc.
- Prisma - Great migrations workflow, but I want to maximize perf.
- Kysely - Great typesafe query builder for SQL, minimally wraps around db connection.
- SQLite/LibSQL (Turso) - Cheapest database, easy to use.
- Lucia Book + Arctic - Makes self-rolling auth easy, and not dependent on any third-party. (You may learn a thing or two with this low-level implementation as well). I chose not to use better-auth, everything is custom built.
- Nodemailer (or any email API/SDK) - Just customize
email-client.ts
. Send emails w/ any API: SMTP or SDK-specific (Amazon SES, Resend, Zeptomail, etc.). Amazon SES is the cheapest. I personally use Zeptomail. Tip: SDK-specific is preferred because SMTP is unreliable for some services because of the handshake requirement. - Backblaze (or any S3) - Cheap blob object storage with an S3-compatible API.
- Dodo Payments - Accept payments and pay foreign taxes, cool new payment tech I found.
I'll assume you don't want to change anything with this setup after cloning so let's get to work!
- Get template
npx degit https://github.com/blankeos/solid-launch <your-app-name>
cd <your-app-name>
-
Copy the environment variables
cp .env.example .env
-
Replace the
<absolute_url>
in the local database with:pwd # If it outputs: /User/Projects/solid-launch # Replace the .env with: DATABASE_URL="file:/User/Projects/solid-launch/local.db"
-
Generate
bun db:generate # generates Kysely and Prisma client types. bun db:migrate # migrates your database.
-
Install deps and run dev
bun install bun dev
I took care of the painstaking parts to help you develop easily on a SPA + SSR + backend paradigm. You can take take these practices to different projects as well.
-
Make use of the
code-snippets
I added. It'll help! -
Check all typescript errors (
Cmd
+Shift
+B
>tsc:watch tsconfig.json
). -
Authentication Practices - I have these out-of-the-box for you so you won't have to build it.
-
Getting Current User
import { useAuthContext } from '@/context/auth.context' export default function MyComponent() { const { user } = useAuthContext() }
-
Login, Logout, Register
import { useAuthContext } from '@/context/auth.context' export default function MyComponent() { const { login, logout, register } = useAuthContext() }
-
Hydrating Current User
This will also automatically hydrate in your layouts. Anywhere you use
useAuthStore()
, it's magic. (Thanks to Vike'suseData()
. Fun fact: You actually can't do this in SolidStart because it's architecturally different to Vike).// +data.ts import { initHonoClient } from '@/lib/hono-client' import { PageContext } from 'vike/types' export type Data = ReturnType<Awaited<typeof data>> export async function data(pageContext: PageContext) { const { urlParsed, request, response } = pageContext const hc = initHonoClient(urlParsed.origin!, { requestHeaders: request.header(), responseHeaders: response.headers, }) const apiResponse = await hc.auth.$get() const result = await apiResponse.json() return { user: result?.user ?? null, } }
-
Protecting Routes (Client-Side)
import ProtectedRoute from '@/components/common/protected-route'; export default MyComponent() { return ( <ProtectedRoute> On the server (hydration), this part will not be rendered if unauthenticated. On the client, you will be redirected to a public route if unauthenticated. </ProtectedRoute> ) }
-
Protecting Routes (SSR)
// +guard.ts (If you don't have +data.ts in the same route). export async function guard(pageContext: PageContext) { const { urlParsed, request, response } = pageContext const hc = initHonoClient(urlParsed.origin!, { requestHeaders: request.header(), responseHeaders: response.headers, }) const apiResponse = await hc.auth.$get() const result = await apiResponse.json() if (!result?.user) { throw redirect('/') // Must be a public route. } } // +guard.ts (If you already have a +data.ts that gets the user). // β οΈ I have not tested this. This depends on `+guard` being called after `+data` is resolved. export async function guard(pageContext: PageContext) { if (!pageContext.data?.user) { throw redirect('/') // Must be a public route. } }
-
-
Dataloading Practices - Also have these out-of-the-box for most usecases since they're tricky to do if you're clueless:
- Tanstack Query (Client-only) - Use
honoClient
from@/lib/hono-client.ts
- Hydrated Tanstack Query (SSR) - Use
create-dehydrated-state.ts
+initHonoClient
- Tanstack Query (Client-only) - Use
My backend architecture is inspired by DDD where I separate in layers, but I keep it pragmatic by not going too overkill with Entities, Domains, and Aggregates. I personally still like the anemic data-driven architecture for most of my apps since the apps I make aren't too business-logic-heavy.
.
βββ server/ # - root
βββ dao/ # - data-access-objects
β βββ *.dao.ts
βββ modules/
β βββ <module>/
β βββ <module>.dao.ts # Plain JS classes with functions for purely reading/writing to database. Like db utils.
β βββ <module>.dto.ts # Zod objects or pure typescript types.
β βββ <module>.service.ts # Plain JS classes with business logic. (Throw api errors, use DAOs, call other services).
β βββ <module>.controller.ts # In charge of validators, REST (GET, POST, etc.), and setting to headers.
βββ _app.ts # - root TRPC router.
dao
- abstracted away all queries here to interface with them as plain functions. It actually helps me mentally collocate db queries from service logic when I'm using them inside the service.modules
- a vertical slice of each module-group. This just depends on how I feel about grouping them. You get better overtime.<module>.controller.ts
- pretty much a group of http endpoints. I can put the DTOs/Validations for each endpoint here without context-switching.services
- these are even smaller pieces of logic that can be used inside each endpoint. It's not necessary to use if the app isn't too big, but it helps._app.ts
- The root trpc router where theAppRouter
type is exported.
Warning
Still in progress
Here are some guides on how to deploy.
- Dokku (self-host VPS - I recommend this)
- Kamal (self-host VPS)
- Railway
- Caprover (self-host VPS)
- Cloudflare (serverless + static)
- Vercel (serverless + static)
- Netlify (static)
- Websockets and Bun
- It works fine in Prod. But activating Vite HMR + Websockets is not possible in Bun.
- This is because Bun doesn't work with
Connect.Server
middlewares (which Vite uses). [Source 1] [Source 2] - Bun Workaround: Having a separate process to run the Websocket server and your HTTP Server. Just make sure to use the same pubsub across these two processes (You can do this using Redis). Also make sure to combine them in a single process in production.
- Alternative recommendation: Use Node instead as it's possible to use
Connect.Server
middlewares in theirhttp
server: PoC.
- Cron jobs, scheduled tasks, heavy background processing, eventual consistency - QStash or Trigger.dev
- Multiplayer and Realtime - Rivet.dev or Convex.dev
- Image optimization pipeline - Sharp for resizing, WebP/AVIF conversion, and caching. Pair with a CDN for global delivery.
- Search that scales - MeiliSearch (self-host) or Algolia (managed). Both have instant search UIs you can drop in.
- Customer Feedback - Userjot
- Customer Support - Chatwoot
- Analytics - PostHog or umami.sh
- Feature flags & A/B tests - Unleash (self-host) or PostHog (product analytics + flags). Roll out safely without redeploys.
- Error & uptime monitoring - Sentry for exceptions, Uptime Kuma for pings. Both can be self-hosted. Glitchtip
- Affiliate Tracker - Refref
I'll probably make a swapping guide soon. To replace to these:
- Runtime: Bun -> Node
- Package Manager: Bun -> PNPM
- ORM: Prisma -> Drizzle
- Database: SQLite -> PostgreSQL, CockroachDB, MongoDB
-
Why not better-auth?
- I thought long and hard about this, and I'm foreseeing a lot of pushback on this so I'll document it here.
- I completely understand the extreme strawman argument of "I want to build an app, so here's the entire OAuth spec to implement it". In almost 99% of usecases, you will choose better-auth to save time, it will be better tested, and will give you more flexibility for easier auth flows for 99% of apps. This Lucia implementation is for that extra flexibility for harder auth flows in 1% of apps--which your next SaaS and mine most likely won't be, so why??
- I initially wrote the template when better-auth wasn't the standard solution, while Lucia was the up and coming one. Lucia actually made me learn about auth more than any resource in my career, so I started to prefer it. Better auth will save you time, but I already spent that time, and this is the flywheel I wrote to save just as much time as using better auth.
- I genuinely believe simple auth isn't so complicated that you'd need a library to abstract it. And for complex auth, you will almost always need a custom solution eventually.
- But for flexibility i.e. changing my server framework, database, etc... This approach won't save me time. Better auth wins there.
- But it will save me time if I want to support an extremely custom auth flow that better auth doesn't support yet. (I have no examples)
- I also save time if I want to implement auth in other languages other than javascript i.e. Rust because the structure and architecture can be done in other languages too.