├── www ├── .nvmrc ├── bun.lockb ├── public │ ├── favicon.ico │ ├── thumbnail.png │ ├── tk_dodo.png │ ├── mylamboreally.png │ ├── create-redis-db.png │ ├── cf-deployment-url.png │ ├── change-api-dirname.png │ ├── wrangler-secret-put.png │ ├── verify-ws-connection.png │ ├── copy-redis-env-variables.png │ └── vercel-environment-variables.png ├── src │ ├── assets │ │ └── JetBrainsMonoNL-Medium.ttf │ ├── types.ts │ ├── app │ │ ├── api │ │ │ └── [[...route]] │ │ │ │ └── route.ts │ │ ├── robots.ts │ │ ├── sitemap.ts │ │ ├── page.tsx │ │ ├── layout.tsx │ │ ├── docs │ │ │ ├── mobile-nav.tsx │ │ │ └── doc-navigation.tsx │ │ └── globals.css │ ├── components │ │ ├── landing │ │ │ ├── footer.tsx │ │ │ ├── search-input.tsx │ │ │ ├── feature-card.tsx │ │ │ ├── stargazer.tsx │ │ │ ├── lambo-section.tsx │ │ │ └── hero-section.tsx │ │ ├── heading-with-ref.tsx │ │ ├── ui │ │ │ ├── input.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── avatar.tsx │ │ │ ├── scroll-area.tsx │ │ │ └── dialog.tsx │ │ ├── providers.tsx │ │ ├── shiny-button.tsx │ │ ├── copy-button.tsx │ │ ├── active-section-observer.tsx │ │ ├── table-of-contents.tsx │ │ └── stars-section.tsx │ ├── ctx │ │ ├── use-debounce.ts │ │ └── use-table-of-contents.ts │ ├── lib │ │ ├── client.ts │ │ └── utils.ts │ ├── server │ │ ├── index.ts │ │ ├── routers │ │ │ ├── search-router.ts │ │ │ └── stargazers-router.ts │ │ └── jstack.ts │ ├── config.ts │ ├── docs │ │ ├── getting-started │ │ │ ├── local-development.mdx │ │ │ └── environment-variables.mdx │ │ ├── backend │ │ │ ├── performance.mdx │ │ │ ├── api-client.mdx │ │ │ ├── routers.mdx │ │ │ └── middleware.mdx │ │ ├── introduction │ │ │ ├── key-features.mdx │ │ │ └── jstack.mdx │ │ └── deploy │ │ │ ├── vercel.mdx │ │ │ └── cloudflare.mdx │ ├── scripts │ │ └── index-docs.ts │ └── actions │ │ └── stargazers.ts ├── wrangler.toml ├── postcss.config.mjs ├── README.md ├── .github │ └── workflows │ │ └── star-notification.yml ├── next.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── content-collections.ts ├── shiki-rehype.mjs ├── package.json └── tailwind.config.ts ├── app ├── src │ ├── app │ │ ├── globals.css │ │ ├── api │ │ │ └── [[...route]] │ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ ├── components │ │ │ ├── providers.tsx │ │ │ └── post.tsx │ │ └── page.tsx │ ├── lib │ │ ├── utils.ts │ │ └── client.ts │ └── server │ │ ├── index.ts │ │ ├── routers │ │ └── post-router.ts │ │ └── jstack.ts ├── .eslintrc.json ├── bun.lockb ├── README.md ├── public │ ├── favicon.ico │ └── noise.svg ├── next.config.mjs ├── postcss.config.mjs ├── .prettierrc ├── drizzle.config.ts ├── wrangler.jsonc ├── .gitignore ├── _gitignore ├── tsconfig.json └── package.json ├── cli ├── template │ ├── extras │ │ ├── config │ │ │ ├── _env-drizzle │ │ │ ├── _env-drizzle-vercel-postgres │ │ │ ├── _prettier.config.js │ │ │ ├── drizzle-config-neon.ts │ │ │ ├── drizzle-config-planetscale.ts │ │ │ ├── drizzle-config-postgres.ts │ │ │ └── drizzle-config-vercel-postgres.ts │ │ └── src │ │ │ └── server │ │ │ ├── jstack │ │ │ ├── base.ts │ │ │ ├── drizzle-with-vercel-postgres.ts │ │ │ ├── drizzle-with-neon.ts │ │ │ ├── drizzle-with-postgres.ts │ │ │ └── drizzle-with-planetscale.ts │ │ │ ├── db │ │ │ └── schema │ │ │ │ ├── with-postgres.ts │ │ │ │ └── with-mysql.ts │ │ │ └── routers │ │ │ └── post │ │ │ ├── base.ts │ │ │ └── with-drizzle.ts │ ├── base │ │ ├── src │ │ │ ├── app │ │ │ │ ├── globals.css │ │ │ │ ├── api │ │ │ │ │ └── [[...route]] │ │ │ │ │ │ └── route.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── components │ │ │ │ │ ├── providers.tsx │ │ │ │ │ └── post.tsx │ │ │ │ └── page.tsx │ │ │ ├── lib │ │ │ │ ├── utils.ts │ │ │ │ └── client.ts │ │ │ └── server │ │ │ │ └── index.ts │ │ ├── README.md │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── noise.svg │ │ ├── next.config.mjs │ │ ├── postcss.config.mjs │ │ ├── wrangler.jsonc │ │ ├── _gitignore │ │ ├── tsconfig.json │ │ └── package.json │ └── base-assets │ │ └── base-package.json ├── bun.lockb ├── .gitignore ├── README.md ├── src │ ├── utils │ │ ├── logger.ts │ │ ├── get-user-pkg-manager.ts │ │ └── add-package-dep.ts │ ├── constants.ts │ ├── installers │ │ ├── dep-version-map.ts │ │ ├── no-orm.ts │ │ ├── neon.ts │ │ ├── planetscale.ts │ │ ├── vercel-postgres.ts │ │ ├── postgres.ts │ │ ├── drizzle.ts │ │ └── index.ts │ ├── helpers │ │ ├── scaffold-project.ts │ │ ├── install-packages.ts │ │ ├── install-deps.ts │ │ └── install-base-template.ts │ ├── index.ts │ └── cli │ │ └── index.ts ├── tsup.config.ts ├── package.json ├── scripts │ └── copy-base-template.ts └── tsconfig.json ├── packages ├── jstack │ ├── src │ │ ├── client │ │ │ ├── index.ts │ │ │ └── hooks │ │ │ │ ├── index.ts │ │ │ │ └── use-web-socket.ts │ │ └── server │ │ │ ├── index.ts │ │ │ ├── middleware │ │ │ ├── utils.ts │ │ │ └── index.ts │ │ │ ├── io.ts │ │ │ ├── dynamic.ts │ │ │ ├── merge-routers.ts │ │ │ ├── types.ts │ │ │ └── j.ts │ ├── bun.lockb │ ├── .gitignore │ ├── tsup.config.ts │ ├── tsconfig.json │ └── package.json └── jstack-shared │ ├── src │ ├── index.ts │ ├── logger.ts │ └── event-emitter.ts │ ├── bun.lockb │ ├── tsup.config.ts │ ├── .gitignore │ ├── tsconfig.json │ └── package.json ├── bun.lockb ├── prettier.config.mjs ├── package.json ├── .gitignore ├── README.md └── LICENSE /www/.nvmrc: -------------------------------------------------------------------------------- 1 | 20.x -------------------------------------------------------------------------------- /app/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /cli/template/extras/config/_env-drizzle: -------------------------------------------------------------------------------- 1 | DATABASE_URL= -------------------------------------------------------------------------------- /packages/jstack/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hooks" 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/bun.lockb -------------------------------------------------------------------------------- /cli/template/base/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /cli/template/extras/config/_env-drizzle-vercel-postgres: -------------------------------------------------------------------------------- 1 | POSTGRES_URL= -------------------------------------------------------------------------------- /app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/app/bun.lockb -------------------------------------------------------------------------------- /cli/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/cli/bun.lockb -------------------------------------------------------------------------------- /packages/jstack/src/client/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-web-socket" 2 | -------------------------------------------------------------------------------- /www/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/bun.lockb -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | ## JStack 2 | 3 | Ship high-performance Next.js apps for extremely cheap 4 | -------------------------------------------------------------------------------- /packages/jstack-shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger" 2 | export * from "./socket" 3 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/app/public/favicon.ico -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/thumbnail.png -------------------------------------------------------------------------------- /www/public/tk_dodo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/tk_dodo.png -------------------------------------------------------------------------------- /packages/jstack/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/packages/jstack/bun.lockb -------------------------------------------------------------------------------- /cli/template/base/README.md: -------------------------------------------------------------------------------- 1 | ## JStack 2 | 3 | Ship high-performance Next.js apps for extremely cheap 4 | -------------------------------------------------------------------------------- /www/public/mylamboreally.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/mylamboreally.png -------------------------------------------------------------------------------- /www/public/create-redis-db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/create-redis-db.png -------------------------------------------------------------------------------- /packages/jstack-shared/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/packages/jstack-shared/bun.lockb -------------------------------------------------------------------------------- /www/public/cf-deployment-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/cf-deployment-url.png -------------------------------------------------------------------------------- /www/public/change-api-dirname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/change-api-dirname.png -------------------------------------------------------------------------------- /www/public/wrangler-secret-put.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/wrangler-secret-put.png -------------------------------------------------------------------------------- /cli/template/base/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/cli/template/base/public/favicon.ico -------------------------------------------------------------------------------- /www/public/verify-ws-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/verify-ws-connection.png -------------------------------------------------------------------------------- /app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | export default nextConfig 5 | -------------------------------------------------------------------------------- /www/public/copy-redis-env-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/copy-redis-env-variables.png -------------------------------------------------------------------------------- /www/src/assets/JetBrainsMonoNL-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/src/assets/JetBrainsMonoNL-Medium.ttf -------------------------------------------------------------------------------- /www/public/vercel-environment-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/jstack/main/www/public/vercel-environment-variables.png -------------------------------------------------------------------------------- /www/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "jstack-www" 2 | compatibility_date = "2024-08-06" 3 | main = "src/server/index.ts" 4 | 5 | [dev] 6 | port = 8080 -------------------------------------------------------------------------------- /app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | } 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /cli/template/base/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | export default nextConfig 5 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": false, 6 | "printWidth": 80 7 | } -------------------------------------------------------------------------------- /cli/template/base/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | } 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | # MAC 5 | ._* 6 | .DS_Store 7 | Thumbs.db 8 | 9 | # env 10 | .env 11 | 12 | # Testing 13 | demo_projects 14 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | This CLI is built by looking at how the [T3 Stack](https://create.t3.gg/) built their CLI. The credit for this CLI and how it's structured goes to them. -------------------------------------------------------------------------------- /www/src/types.ts: -------------------------------------------------------------------------------- 1 | export type SearchMetadata = { 2 | title: string 3 | path: string 4 | level: number 5 | type: string 6 | content: string 7 | documentTitle: string 8 | } 9 | -------------------------------------------------------------------------------- /www/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /cli/template/extras/config/_prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | } 5 | -------------------------------------------------------------------------------- /www/src/app/api/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import appRouter from "@/server" 2 | import { handle } from "hono/vercel" 3 | 4 | export const GET = handle(appRouter.handler) 5 | export const POST = handle(appRouter.handler) 6 | -------------------------------------------------------------------------------- /cli/template/base/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/jstack/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import "./types" 2 | 3 | export * from "./types" 4 | export * from "./j" 5 | export * from "./procedure" 6 | export * from "./router" 7 | export * from "./client" 8 | export * from "./dynamic" 9 | -------------------------------------------------------------------------------- /packages/jstack/src/server/middleware/utils.ts: -------------------------------------------------------------------------------- 1 | import superjson from "superjson" 2 | 3 | export const parseSuperJSON = (value: string) => { 4 | try { 5 | return superjson.parse(value) 6 | } catch { 7 | return value 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | ## JStack 2 | 3 | Ship high-performance Next.js apps for extremely cheap 4 | 5 | Stuff to improve 6 | - footer padding 7 | - docs "on this page" visual bug 8 | - add docs for: 9 | - procedures 10 | - cloudflare deployment 11 | - vercel deployment -------------------------------------------------------------------------------- /app/src/app/api/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import appRouter from "@/server" 2 | import { handle } from "hono/vercel" 3 | 4 | // This route catches all incoming API requests and lets your appRouter handle them. 5 | export const GET = handle(appRouter.handler) 6 | export const POST = handle(appRouter.handler) 7 | -------------------------------------------------------------------------------- /packages/jstack-shared/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["src/index.ts"], 6 | outDir: "dist/", 7 | format: ["esm", "cjs"], 8 | dts: true, 9 | clean: false, 10 | minify: false, 11 | }, 12 | ]) 13 | -------------------------------------------------------------------------------- /app/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config" 2 | import { defineConfig } from "drizzle-kit" 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL ?? "", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cli/template/base/src/app/api/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import appRouter from "@/server" 2 | import { handle } from "hono/vercel" 3 | 4 | // This route catches all incoming API requests and lets your appRouter handle them. 5 | export const GET = handle(appRouter.handler) 6 | export const POST = handle(appRouter.handler) 7 | -------------------------------------------------------------------------------- /app/src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@/server" 2 | import { createClient } from "jstack" 3 | 4 | /** 5 | * Your type-safe API client 6 | * @see https://jstack.app/docs/backend/api-client 7 | */ 8 | export const client = createClient({ 9 | baseUrl: "http://localhost:3000/api", 10 | }) 11 | -------------------------------------------------------------------------------- /www/src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next" 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | disallow: "/api", 9 | }, 10 | sitemap: "https://jstack.app/sitemap.xml", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack-app", 3 | "main": "src/server/index.ts", 4 | "compatibility_date": "2025-02-14", 5 | "observability": { 6 | "enabled": true 7 | }, 8 | "placement": { 9 | "mode": "smart" 10 | }, 11 | "dev": { 12 | "port": 8080 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cli/template/base/src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@/server" 2 | import { createClient } from "jstack" 3 | 4 | /** 5 | * Your type-safe API client 6 | * @see https://jstack.app/docs/backend/api-client 7 | */ 8 | export const client = createClient({ 9 | baseUrl: "http://localhost:3000/api", 10 | }) 11 | -------------------------------------------------------------------------------- /cli/template/base/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack-app", 3 | "main": "src/server/index.ts", 4 | "compatibility_date": "2025-02-14", 5 | "observability": { 6 | "enabled": true 7 | }, 8 | "placement": { 9 | "mode": "smart" 10 | }, 11 | "dev": { 12 | "port": 8080 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cli/template/extras/config/drizzle-config-neon.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit" 2 | import "dotenv/config" 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/server/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL ?? "", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cli/template/extras/config/drizzle-config-planetscale.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit" 2 | import "dotenv/config" 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/server/db/schema.ts", 7 | dialect: "mysql", 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL ?? "", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cli/template/extras/config/drizzle-config-postgres.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit" 2 | import "dotenv/config" 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/server/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL ?? "", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /www/.github/workflows/star-notification.yml: -------------------------------------------------------------------------------- 1 | on: 2 | watch: 3 | types: [started] 4 | 5 | jobs: 6 | notify: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Send notification 10 | run: | 11 | curl -X POST -H "Authorization: ${{ secrets.AUTH_TOKEN }}" https://stargazers.joshtriedcoding.workers.dev/api/stargazers/refresh -------------------------------------------------------------------------------- /cli/template/extras/config/drizzle-config-vercel-postgres.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit" 2 | import "dotenv/config" 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/server/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: process.env.POSTGRES_URL ?? "", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig} 3 | */ 4 | const config = { 5 | arrowParens: "always", 6 | printWidth: 80, 7 | singleQuote: false, 8 | jsxSingleQuote: false, 9 | semi: false, 10 | trailingComma: "all", 11 | tabWidth: 2, 12 | }; 13 | 14 | export default config; -------------------------------------------------------------------------------- /app/public/noise.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/jstack/base.ts: -------------------------------------------------------------------------------- 1 | import { jstack } from "jstack" 2 | 3 | interface Env { 4 | Bindings: {} 5 | } 6 | 7 | export const j = jstack.init() 8 | 9 | /** 10 | * Public (unauthenticated) procedures 11 | * 12 | * This is the base piece you use to build new queries and mutations on your API. 13 | */ 14 | export const publicProcedure = j.procedure 15 | -------------------------------------------------------------------------------- /cli/template/base/public/noise.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /cli/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | 3 | export const logger = { 4 | error(...args: unknown[]) { 5 | console.log(chalk.red(...args)) 6 | }, 7 | warn(...args: unknown[]) { 8 | console.log(chalk.yellow(...args)) 9 | }, 10 | info(...args: unknown[]) { 11 | console.log(chalk.cyan(...args)) 12 | }, 13 | success(...args: unknown[]) { 14 | console.log(chalk.green(...args)) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /cli/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | const isDev = process.env.npm_lifecycle_event === "dev" 4 | 5 | export default defineConfig({ 6 | clean: true, 7 | entry: ["src/index.ts"], 8 | format: ["esm"], 9 | minify: !isDev, 10 | target: "esnext", 11 | outDir: "dist", 12 | onSuccess: isDev ? "node dist/index.js" : undefined, 13 | banner: { 14 | js: '#!/usr/bin/env node', 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /www/src/components/landing/footer.tsx: -------------------------------------------------------------------------------- 1 | import { HeartIcon } from "lucide-react" 2 | 3 | export const Footer = () => { 4 | return ( 5 |
6 |

7 | JStack, a full-stack Next.js & TypeScript community project{" "} 8 | 9 |

10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /www/src/ctx/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export function useDebounce(value: T, delay: number = 500): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, delay) 10 | 11 | return () => { 12 | clearTimeout(timer) 13 | } 14 | }, [value, delay]) 15 | 16 | return debouncedValue 17 | } 18 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/db/schema/with-postgres.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, serial, text, timestamp, index } from "drizzle-orm/pg-core" 2 | 3 | export const posts = pgTable( 4 | "posts", 5 | { 6 | id: serial("id").primaryKey(), 7 | name: text("name").notNull(), 8 | createdAt: timestamp("createdAt").defaultNow().notNull(), 9 | updatedAt: timestamp("updatedAt").defaultNow().notNull(), 10 | }, 11 | (table) => [ 12 | index("Post_name_idx").on(table.name) 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /packages/jstack/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp 4 | .pnp.js 5 | 6 | # Build output 7 | dist/ 8 | build/ 9 | lib/ 10 | *.tgz 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor and IDE files 20 | .idea/ 21 | .vscode/ 22 | *.swp 23 | *.swo 24 | .DS_Store 25 | 26 | # Test coverage 27 | coverage/ 28 | 29 | # Environment variables 30 | .env 31 | .env.local 32 | .env.*.local 33 | 34 | # npm cache 35 | .npm 36 | -------------------------------------------------------------------------------- /packages/jstack-shared/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp 4 | .pnp.js 5 | 6 | # Build output 7 | dist/ 8 | build/ 9 | lib/ 10 | *.tgz 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor and IDE files 20 | .idea/ 21 | .vscode/ 22 | *.swp 23 | *.swo 24 | .DS_Store 25 | 26 | # Test coverage 27 | coverage/ 28 | 29 | # Environment variables 30 | .env 31 | .env.local 32 | .env.*.local 33 | 34 | # npm cache 35 | .npm 36 | -------------------------------------------------------------------------------- /www/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withContentCollections } from "@content-collections/next" 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | experimental: { 6 | inlineCss: true, 7 | }, 8 | redirects: async () => { 9 | return [ 10 | { 11 | source: "/docs", 12 | destination: "/docs/getting-started/first-steps", 13 | permanent: true, 14 | }, 15 | ] 16 | }, 17 | } 18 | 19 | export default withContentCollections(nextConfig) 20 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/db/schema/with-mysql.ts: -------------------------------------------------------------------------------- 1 | import { mysqlTable, serial, varchar, timestamp, index } from "drizzle-orm/mysql-core" 2 | 3 | export const posts = mysqlTable( 4 | "posts", 5 | { 6 | id: serial("id").primaryKey(), 7 | name: varchar("name", { length: 255 }).notNull(), 8 | createdAt: timestamp("createdAt").defaultNow().notNull(), 9 | updatedAt: timestamp("updatedAt").defaultNow().notNull(), 10 | }, 11 | (table) => [ 12 | index("Post_name_idx").on(table.name) 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /packages/jstack/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["src/server/index.ts"], 6 | outDir: "dist/server", 7 | format: ["esm", "cjs"], 8 | dts: true, 9 | clean: false, 10 | minify: false, 11 | external: ["zod"], 12 | }, 13 | { 14 | entry: ["src/client/index.ts"], 15 | outDir: "dist/client", 16 | format: ["esm", "cjs"], 17 | dts: true, 18 | clean: false, 19 | minify: false, 20 | external: ["zod"], 21 | }, 22 | ]) 23 | -------------------------------------------------------------------------------- /www/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /www/src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@/server" 2 | import { createClient } from "jstack" 3 | 4 | export const client = createClient({ 5 | baseUrl: `${getBaseUrl()}/api`, 6 | }) 7 | 8 | function getBaseUrl() { 9 | if (typeof window !== "undefined") return window.location.origin 10 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` 11 | if (process.env.NODE_ENV === "production") return `https://${process.env.AMPLIFY_URL}` 12 | return `http://localhost:${process.env.PORT ?? 3000}` 13 | } 14 | -------------------------------------------------------------------------------- /cli/src/constants.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { fileURLToPath } from "url" 3 | 4 | // With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily. 5 | // Path is in relation to a single index.js file inside ./dist 6 | const __filename = fileURLToPath(import.meta.url) 7 | const distPath = path.dirname(__filename) 8 | 9 | export const PKG_ROOT = path.join(distPath, "../") 10 | export const DEFAULT_APP_NAME = "my-jstack-app" 11 | export const CREATE_JSTACK_APP = "create-jstack-app" 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack", 3 | "version": "0.0.0", 4 | "description": "The stack for building seriously fast, lightweight and end-to-end typesafe Next.js apps.", 5 | "author": "Josh tried coding", 6 | "type": "module", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/upstash/jstack.git" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": ["jstack", "next.js", "typescript", "hono"], 16 | "dependencies": { 17 | "prettier": "^3.4.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Providers } from "./components/providers" 3 | 4 | import "./globals.css" 5 | 6 | export const metadata: Metadata = { 7 | title: "JStack App", 8 | description: "Created using JStack", 9 | icons: [{ rel: "icon", url: "/favicon.ico" }], 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode 16 | }>) { 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | .vscode 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env 31 | .env*.local 32 | .dev.vars 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # wrangler 42 | .wrangler 43 | -------------------------------------------------------------------------------- /www/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import type { InferRouterOutputs } from "jstack" 2 | import { j } from "./jstack" 3 | import { searchRouter } from "./routers/search-router" 4 | import { stargazersRouter } from "./routers/stargazers-router" 5 | 6 | export const api = j 7 | .router() 8 | .basePath("/api") 9 | .use(j.defaults.cors) 10 | .onError(j.defaults.errorHandler) 11 | 12 | const appRouter = j.mergeRouters(api, { 13 | search: searchRouter, 14 | stargazers: stargazersRouter, 15 | }) 16 | 17 | export type AppRouter = typeof appRouter 18 | export default appRouter 19 | 20 | export type InferOutput = InferRouterOutputs 21 | -------------------------------------------------------------------------------- /cli/template/base/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Providers } from "./components/providers" 3 | 4 | import "./globals.css" 5 | 6 | export const metadata: Metadata = { 7 | title: "JStack App", 8 | description: "Created using JStack", 9 | icons: [{ rel: "icon", url: "/favicon.ico" }], 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode 16 | }>) { 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /www/src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { allDocs } from "content-collections" 2 | import type { MetadataRoute } from "next" 3 | 4 | const BASE_URL = "https://jstack.app" 5 | 6 | export default function sitemap(): MetadataRoute.Sitemap { 7 | const paths = allDocs.map((doc) => `${BASE_URL}/docs/${doc._meta.path}`) 8 | 9 | return [ 10 | { url: "https://jstack.app/", lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.5 }, 11 | ...paths.map((path) => ({ 12 | url: path, 13 | lastModified: new Date(), 14 | changeFrequency: "monthly" as const, 15 | priority: 0.5, 16 | })), 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | /dist 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | .vscode 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env 32 | .env*.local 33 | .dev.vars 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # wrangler 43 | .wrangler 44 | -------------------------------------------------------------------------------- /app/_gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | /dist 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | .vscode 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env 32 | .env*.local 33 | .dev.vars 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # wrangler 43 | .wrangler 44 | -------------------------------------------------------------------------------- /packages/jstack-shared/src/logger.ts: -------------------------------------------------------------------------------- 1 | export const logger = { 2 | info(message: string, ...args: any[]) { 3 | console.log(`[Socket] ℹ️ ${message}`, ...args) 4 | }, 5 | 6 | error(message: string, error?: Error | unknown) { 7 | console.error(`[Socket] ❌ ${message}`, error || "") 8 | }, 9 | 10 | debug(message: string, ...args: any[]) { 11 | console.log(`[Socket] 🔍 ${message}`, ...args) 12 | }, 13 | 14 | warn(message: string, ...args: any[]) { 15 | console.warn(`[Socket] ⚠️ ${message}`, ...args) 16 | }, 17 | 18 | success(message: string, ...args: any[]) { 19 | console.log(`[Socket] ✅ ${message}`, ...args) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /cli/template/base/_gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | /dist 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | .vscode 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env 32 | .env*.local 33 | .dev.vars 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # wrangler 43 | .wrangler 44 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | /dist 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | .vscode 24 | .content-collections 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env 33 | .env*.local 34 | .dev.vars 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # wrangler 44 | .wrangler 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JStack - Build fast, lightweight and end-to-end typesafe Next.js apps 2 | 3 | Built on Next.js 15, Hono, Tailwind and Drizzle ORM. 4 | 5 | ![Project Thumbnail](https://github.com/upstash/jstack/blob/main/www/public/thumbnail.png) 6 | 7 | ## About 8 | 9 | Appreciate you for checking out JStack! For features & docs, see: 10 | 11 | https://jstack.app/ 12 | 13 | Cheers, 14 | Josh 15 | 16 | ### Acknowledgements 17 | 18 | - [T3 Stack](https://github.com/t3-oss/create-t3-app) 19 | - [tRPC](https://trpc.io/) 20 | - [Hono](https://hono.dev/) 21 | 22 | and all supporters/contributors. y'all rock 23 | 24 | ## License 25 | 26 | [MIT](https://choosealicense.com/licenses/mit/) 27 | -------------------------------------------------------------------------------- /www/src/server/routers/search-router.ts: -------------------------------------------------------------------------------- 1 | import { SearchMetadata } from "@/types" 2 | import { z } from "zod" 3 | import { j, publicProcedure, vectorMiddleware } from "../jstack" 4 | 5 | export const searchRouter = j.router({ 6 | byQuery: publicProcedure 7 | .use(vectorMiddleware) 8 | .input(z.object({ query: z.string().min(1).max(1000) })) 9 | .get(async ({ c, ctx, input }) => { 10 | const { index } = ctx 11 | const { query } = input 12 | 13 | const res = await index.query({ 14 | topK: 10, 15 | data: query, 16 | includeMetadata: true, 17 | }) 18 | 19 | return c.superjson(res) 20 | }), 21 | }) 22 | -------------------------------------------------------------------------------- /www/src/components/landing/search-input.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from "lucide-react" 2 | import React from "react" 3 | 4 | export const SearchInput = () => { 5 | return ( 6 |
7 | 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/jstack-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "lib": ["ESNext"], 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "resolveJsonModule": true, 10 | "jsx": "react-jsx", 11 | "jsxImportSource": "react", 12 | "outDir": "./dist", 13 | "noEmit": true, 14 | "types": ["@types/node"], 15 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | } 19 | }, 20 | "include": ["src/**/*", "./package.json"], 21 | "exclude": ["src/**/*.test.ts", "node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /www/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { FeatureSection } from "@/components/landing/feature-section" 2 | import { Footer } from "@/components/landing/footer" 3 | import { HeroSection } from "@/components/landing/hero-section" 4 | import { LamboSection } from "@/components/landing/lambo-section" 5 | import { Navbar } from "@/components/landing/navbar" 6 | import { StarsSection } from "@/components/stars-section" 7 | 8 | export default function Page() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /www/src/components/heading-with-ref.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ReactNode, useRef } from "react" 4 | import { useIntersectionObserver } from "@uidotdev/usehooks" 5 | 6 | interface HeadingProps { 7 | id?: string 8 | level: 1 | 2 | 3 9 | title: string 10 | children: ReactNode 11 | className: string 12 | } 13 | 14 | export function HeadingWithRef({ id, level, title, children, className }: HeadingProps) { 15 | const ref = useRef(null) 16 | const headingId = id 17 | 18 | const Component = `h${level}` as const 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /cli/src/utils/get-user-pkg-manager.ts: -------------------------------------------------------------------------------- 1 | export type PackageManager = "npm" | "pnpm" | "yarn" | "bun" 2 | 3 | export const getUserPkgManager: () => PackageManager = () => { 4 | // This environment variable is set by npm and yarn but pnpm seems less consistent 5 | const userAgent = process.env.npm_config_user_agent 6 | 7 | if (userAgent) { 8 | if (userAgent.startsWith("yarn")) { 9 | return "yarn" 10 | } else if (userAgent.startsWith("pnpm")) { 11 | return "pnpm" 12 | } else if (userAgent.startsWith("bun")) { 13 | return "bun" 14 | } else { 15 | return "npm" 16 | } 17 | } else { 18 | // If no user agent is set, assume npm 19 | return "npm" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /www/src/components/landing/feature-card.tsx: -------------------------------------------------------------------------------- 1 | export type FeatureCardProps = { 2 | icon: React.ReactNode 3 | title: string 4 | description: string 5 | } 6 | 7 | export const FeatureCard = ({ icon, title, description }: FeatureCardProps) => { 8 | return ( 9 |
10 |
11 |
{icon}
12 | 13 |

{title}

14 |

{description}

15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { j } from "./jstack" 2 | import { postRouter } from "./routers/post-router" 3 | 4 | /** 5 | * This is your base API. 6 | * Here, you can handle errors, not-found responses, cors and more. 7 | * 8 | * @see https://jstack.app/docs/backend/app-router 9 | */ 10 | const api = j 11 | .router() 12 | .basePath("/api") 13 | .use(j.defaults.cors) 14 | .onError(j.defaults.errorHandler) 15 | 16 | /** 17 | * This is the main router for your server. 18 | * All routers in /server/routers should be added here manually. 19 | */ 20 | const appRouter = j.mergeRouters(api, { 21 | post: postRouter, 22 | }) 23 | 24 | export type AppRouter = typeof appRouter 25 | 26 | export default appRouter 27 | -------------------------------------------------------------------------------- /cli/template/base/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { j } from "./jstack" 2 | import { postRouter } from "./routers/post-router" 3 | 4 | /** 5 | * This is your base API. 6 | * Here, you can handle errors, not-found responses, cors and more. 7 | * 8 | * @see https://jstack.app/docs/backend/app-router 9 | */ 10 | const api = j 11 | .router() 12 | .basePath("/api") 13 | .use(j.defaults.cors) 14 | .onError(j.defaults.errorHandler) 15 | 16 | /** 17 | * This is the main router for your server. 18 | * All routers in /server/routers should be added here manually. 19 | */ 20 | const appRouter = j.mergeRouters(api, { 21 | post: postRouter, 22 | }) 23 | 24 | export type AppRouter = typeof appRouter 25 | 26 | export default appRouter 27 | -------------------------------------------------------------------------------- /packages/jstack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "strict": true, 5 | "lib": ["ES2019", "DOM", "DOM.Iterable", "ES2015.Iterable"], 6 | "allowSyntheticDefaultImports": true, 7 | "resolveJsonModule": true, 8 | "jsx": "react-jsx", 9 | "jsxImportSource": "react", 10 | "module": "ES2022", 11 | "moduleResolution": "node", 12 | "outDir": "./dist", 13 | "noEmit": true, 14 | "types": ["@cloudflare/workers-types/2023-07-01", "@types/node"], 15 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 16 | "paths": { 17 | "@/*": ["./src/react/*"] 18 | } 19 | }, 20 | "include": ["src/**/*", "./package.json"], 21 | "exclude": ["src/**/*.test.ts", "node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /cli/src/installers/dep-version-map.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This maps the necessary packages to a version. 3 | * This improves performance significantly over fetching it from the npm registry. 4 | */ 5 | export const dependencyVersionMap = { 6 | // neon 7 | "@neondatabase/serverless": "^0.10.4", 8 | 9 | // vercel postgres 10 | "@vercel/postgres": "^0.10.0", 11 | 12 | // Drizzle 13 | "drizzle-kit": "^0.30.1", 14 | "drizzle-orm": "^0.39.0", 15 | "eslint-plugin-drizzle": "^0.2.3", 16 | "@planetscale/database": "^1.19.0", 17 | postgres: "^3.4.5", 18 | 19 | // TailwindCSS 20 | tailwindcss: "^4.0.0", 21 | postcss: "^8.5.1", 22 | prettier: "^3.3.2", 23 | "prettier-plugin-tailwindcss": "^0.6.11", 24 | } as const 25 | export type AvailableDependencies = keyof typeof dependencyVersionMap 26 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/routers/post/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { j, publicProcedure } from "../jstack" 3 | 4 | // Mocked DB 5 | interface Post { 6 | id: number 7 | name: string 8 | } 9 | 10 | const posts: Post[] = [ 11 | { 12 | id: 1, 13 | name: "Hello World", 14 | }, 15 | ] 16 | 17 | export const postRouter = j.router({ 18 | recent: publicProcedure.query(({ c }) => { 19 | return c.superjson(posts.at(-1) ?? null) 20 | }), 21 | 22 | create: publicProcedure 23 | .input(z.object({ name: z.string().min(1) })) 24 | .mutation(({ c, input }) => { 25 | const post: Post = { 26 | id: posts.length + 1, 27 | name: input.name, 28 | } 29 | 30 | posts.push(post) 31 | 32 | return c.superjson(post) 33 | }), 34 | }) 35 | -------------------------------------------------------------------------------- /app/src/app/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | QueryCache, 5 | QueryClient, 6 | QueryClientProvider, 7 | } from "@tanstack/react-query" 8 | import { HTTPException } from "hono/http-exception" 9 | import { PropsWithChildren, useState } from "react" 10 | 11 | export const Providers = ({ children }: PropsWithChildren) => { 12 | const [queryClient] = useState( 13 | () => 14 | new QueryClient({ 15 | queryCache: new QueryCache({ 16 | onError: (err) => { 17 | if (err instanceof HTTPException) { 18 | // global error handling, e.g. toast notification ... 19 | } 20 | }, 21 | }), 22 | }) 23 | ) 24 | 25 | return ( 26 | {children} 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /www/src/ctx/use-table-of-contents.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | 3 | interface Heading { 4 | level: number 5 | text: string 6 | } 7 | 8 | interface State { 9 | allHeadings: Heading[] 10 | activeHeadingIds: number[] 11 | visibleSections: Array 12 | setVisibleSections: (visibleSections: Array) => void 13 | setAllHeadings: (headings: Heading[]) => void 14 | } 15 | 16 | export const useTableOfContents = create()((set) => ({ 17 | allHeadings: [], 18 | activeHeadingIds: [], 19 | setAllHeadings: (allHeadings) => set((state) => ({ allHeadings })), 20 | sections: [], 21 | visibleSections: [], 22 | setVisibleSections: (visibleSections) => 23 | set((state) => (state.visibleSections.join() === visibleSections.join() ? {} : { visibleSections })), 24 | })) 25 | -------------------------------------------------------------------------------- /cli/src/installers/no-orm.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "path" 3 | 4 | import { Installer } from "./index.js" 5 | import { PKG_ROOT } from "@/constants.js" 6 | 7 | export const noOrmInstaller: Installer = ({ projectDir }) => { 8 | const extrasDir = path.join(PKG_ROOT, "template/extras") 9 | 10 | const routerSrc = path.join(extrasDir, `src/server/routers/post/base.ts`) 11 | const routerDest = path.join(projectDir, `src/server/routers/post-router.ts`) 12 | 13 | const jstackSrc = path.join(extrasDir, "src/server/jstack", `base.ts`) 14 | const jstackDest = path.join(projectDir, "src/server/jstack.ts") 15 | 16 | fs.ensureDirSync(path.dirname(routerDest)) 17 | fs.ensureDirSync(path.dirname(jstackDest)) 18 | 19 | fs.copySync(routerSrc, routerDest) 20 | fs.copySync(jstackSrc, jstackDest) 21 | } 22 | -------------------------------------------------------------------------------- /cli/template/base/src/app/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | QueryCache, 5 | QueryClient, 6 | QueryClientProvider, 7 | } from "@tanstack/react-query" 8 | import { HTTPException } from "hono/http-exception" 9 | import { PropsWithChildren, useState } from "react" 10 | 11 | export const Providers = ({ children }: PropsWithChildren) => { 12 | const [queryClient] = useState( 13 | () => 14 | new QueryClient({ 15 | queryCache: new QueryCache({ 16 | onError: (err) => { 17 | if (err instanceof HTTPException) { 18 | // global error handling, e.g. toast notification ... 19 | } 20 | }, 21 | }), 22 | }) 23 | ) 24 | 25 | return ( 26 | {children} 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/jstack/drizzle-with-vercel-postgres.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "@vercel/postgres" 2 | import { drizzle } from "drizzle-orm/vercel-postgres" 3 | import { jstack } from "jstack" 4 | 5 | interface Env { 6 | Bindings: { POSTGRES_URL: string } 7 | } 8 | 9 | export const j = jstack.init() 10 | 11 | /** 12 | * Type-safely injects database into all procedures 13 | * 14 | * @see https://jstack.app/docs/backend/middleware 15 | */ 16 | const databaseMiddleware = j.middleware(async ({ next }) => { 17 | // automatically reads POSTGRES_URL environment variable 18 | const db = drizzle(sql) 19 | 20 | return await next({ db }) 21 | }) 22 | 23 | /** 24 | * Public (unauthenticated) procedures 25 | * 26 | * This is the base piece you use to build new queries and mutations on your API. 27 | */ 28 | export const publicProcedure = j.procedure.use(databaseMiddleware) 29 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "noUncheckedIndexedAccess": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "types": ["@cloudflare/workers-types/2023-07-01"], 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | }, 25 | "target": "ES2017" 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts", 32 | "**/*.d.ts" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/routers/post/with-drizzle.ts: -------------------------------------------------------------------------------- 1 | import { posts } from "@/server/db/schema" 2 | import { desc } from "drizzle-orm" 3 | import { z } from "zod" 4 | import { j, publicProcedure } from "../jstack" 5 | 6 | export const postRouter = j.router({ 7 | recent: publicProcedure.query(async ({ c, ctx }) => { 8 | const { db } = ctx 9 | 10 | const [recentPost] = await db 11 | .select() 12 | .from(posts) 13 | .orderBy(desc(posts.createdAt)) 14 | .limit(1) 15 | 16 | return c.superjson(recentPost ?? null) 17 | }), 18 | 19 | create: publicProcedure 20 | .input(z.object({ name: z.string().min(1) })) 21 | .mutation(async ({ ctx, c, input }) => { 22 | const { name } = input 23 | const { db } = ctx 24 | 25 | const post = await db.insert(posts).values({ name }) 26 | 27 | return c.superjson(post) 28 | }), 29 | }) 30 | -------------------------------------------------------------------------------- /www/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /www/src/config.ts: -------------------------------------------------------------------------------- 1 | export const DOCS_CONFIG = { 2 | categories: { 3 | introduction: { 4 | title: "Introduction", 5 | emoji: "🐥", 6 | order: 1, 7 | items: ["why-jstack", "key-features"], 8 | }, 9 | "getting-started": { 10 | title: "Getting Started", 11 | emoji: "👷‍♂️", 12 | order: 2, 13 | items: ["first-steps", "local-development", "environment-variables"], 14 | }, 15 | backend: { 16 | title: "Backend", 17 | emoji: "⚙️", 18 | order: 3, 19 | items: [ 20 | "app-router", 21 | "routers", 22 | "procedures", 23 | "api-client", 24 | "middleware", 25 | "websockets", 26 | "performance" 27 | ], 28 | }, 29 | deploy: { 30 | title: "Deploy", 31 | emoji: "💻", 32 | order: 4, 33 | items: ["vercel, cloudflare"], 34 | }, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /cli/template/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "noUncheckedIndexedAccess": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "types": ["@cloudflare/workers-types/2023-07-01"], 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | }, 25 | "target": "ES2017" 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts", 32 | "**/*.d.ts" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "noUncheckedIndexedAccess": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "types": ["@cloudflare/workers-types/2023-07-01"], 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"], 24 | "content-collections": ["./.content-collections/generated"] 25 | }, 26 | "target": "ES2018" 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "**/*.d.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/jstack/drizzle-with-neon.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless" 2 | import { drizzle } from "drizzle-orm/neon-http" 3 | import { env } from "hono/adapter" 4 | import { jstack } from "jstack" 5 | 6 | interface Env { 7 | Bindings: { DATABASE_URL: string } 8 | } 9 | 10 | export const j = jstack.init() 11 | 12 | /** 13 | * Type-safely injects database into all procedures 14 | * 15 | * @see https://jstack.app/docs/backend/middleware 16 | */ 17 | const databaseMiddleware = j.middleware(async ({ c, next }) => { 18 | const { DATABASE_URL } = env(c) 19 | 20 | const sql = neon(DATABASE_URL) 21 | const db = drizzle(sql) 22 | 23 | return await next({ db }) 24 | }) 25 | 26 | /** 27 | * Public (unauthenticated) procedures 28 | * 29 | * This is the base piece you use to build new queries and mutations on your API. 30 | */ 31 | export const publicProcedure = j.procedure.use(databaseMiddleware) 32 | -------------------------------------------------------------------------------- /app/src/server/routers/post-router.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { j } from "../jstack" 3 | 4 | interface Post { 5 | id: number 6 | name: string 7 | } 8 | 9 | // Mocked DB 10 | const posts: Post[] = [ 11 | { 12 | id: 1, 13 | name: "Hello World", 14 | }, 15 | ] 16 | 17 | /** 18 | * This is a router that defines multiple procedures (`recent`, `create`) 19 | * Under the hood, each procedure is its own HTTP endpoint 20 | * 21 | * @see https://jstack.app/docs/backend/routers 22 | */ 23 | export const postRouter = j.router({ 24 | recent: j.procedure.query(({ c }) => { 25 | return c.superjson(posts.at(-1) ?? null) 26 | }), 27 | 28 | create: j.procedure 29 | .input(z.object({ name: z.string().min(1) })) 30 | .mutation(({ c, input }) => { 31 | const post: Post = { 32 | id: posts.length + 1, 33 | name: input.name, 34 | } 35 | 36 | posts.push(post) 37 | 38 | return c.superjson(post) 39 | }), 40 | }) 41 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/jstack/drizzle-with-postgres.ts: -------------------------------------------------------------------------------- 1 | import { jstack } from "jstack" 2 | import { drizzle } from "drizzle-orm/postgres-js" 3 | import { env } from "hono/adapter" 4 | 5 | interface Env { 6 | Bindings: { DATABASE_URL: string } 7 | } 8 | 9 | export const j = jstack.init() 10 | 11 | /** 12 | * Type-safely injects database into all procedures 13 | * @see https://jstack.app/docs/backend/middleware 14 | * 15 | * For deployment to Cloudflare Workers 16 | * @see https://developers.cloudflare.com/workers/tutorials/postgres/ 17 | */ 18 | const databaseMiddleware = j.middleware(async ({ c, next }) => { 19 | const { DATABASE_URL } = env(c) 20 | 21 | const db = drizzle(DATABASE_URL) 22 | 23 | return await next({ db }) 24 | }) 25 | 26 | /** 27 | * Public (unauthenticated) procedures 28 | * 29 | * This is the base piece you use to build new queries and mutations on your API. 30 | */ 31 | export const publicProcedure = j.procedure.use(databaseMiddleware) 32 | -------------------------------------------------------------------------------- /app/src/server/jstack.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless" 2 | import { drizzle } from "drizzle-orm/neon-http" 3 | import { env } from "hono/adapter" 4 | import { jstack } from "jstack" 5 | 6 | interface Env { 7 | Bindings: { DATABASE_URL: string } 8 | } 9 | 10 | export const j = jstack.init() 11 | 12 | /** 13 | * Injects database instance into all procedures 14 | * 15 | * @example 16 | * ```ts 17 | * publicProcedure.query(({ ctx }) => { 18 | * const { db } = ctx 19 | * return db.select().from(users) 20 | * }) 21 | * ``` 22 | */ 23 | export const databaseMiddleware = j.middleware(async ({ c, next }) => { 24 | const { DATABASE_URL } = env(c) 25 | 26 | const sql = neon(DATABASE_URL) 27 | const db = drizzle(sql) 28 | 29 | return await next({ db }) 30 | }) 31 | 32 | /** 33 | * Public (unauthenticated) procedures 34 | * 35 | * This is the base piece you use to build new queries and mutations on your API. 36 | */ 37 | export const publicProcedure = j.procedure.use(databaseMiddleware) 38 | -------------------------------------------------------------------------------- /cli/template/extras/src/server/jstack/drizzle-with-planetscale.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/planetscale-serverless" 2 | import { env } from "hono/adapter" 3 | import { Client } from "@planetscale/database" 4 | import { jstack } from "jstack" 5 | 6 | interface Env { 7 | Bindings: { DATABASE_URL: string } 8 | } 9 | 10 | export const j = jstack.init() 11 | 12 | /** 13 | * Type-safely injects database into all procedures 14 | * 15 | * @see https://jstack.app/docs/backend/middleware 16 | */ 17 | const databaseMiddleware = j.middleware(async ({ c, next }) => { 18 | const { DATABASE_URL } = env(c) 19 | 20 | const client = new Client({ url: DATABASE_URL }) 21 | const db = drizzle(client) 22 | 23 | return await next({ db }) 24 | }) 25 | 26 | /** 27 | * Public (unauthenticated) procedures 28 | * 29 | * This is the base piece you use to build new queries and mutations on your API. 30 | */ 31 | export const baseProcedure = j.procedure 32 | export const publicProcedure = baseProcedure.use(databaseMiddleware) 33 | -------------------------------------------------------------------------------- /cli/template/base-assets/base-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@tailwindcss/postcss": "^4.0.0", 13 | "@tanstack/react-query": "^5.51.23", 14 | "clsx": "^2.1.1", 15 | "hono": "^4.7.0", 16 | "jstack": "^1.1.1", 17 | "next": "^15.1.6", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "superjson": "^2.2.1", 21 | "tailwind-merge": "^2.5.2", 22 | "zod": "^3.24.1" 23 | }, 24 | "devDependencies": { 25 | "dotenv": "^16.4.5", 26 | "@cloudflare/workers-types": "^4.20250214.0", 27 | "@types/node": "^22.10.6", 28 | "@types/react": "^19.0.7", 29 | "@types/react-dom": "^19.0.3", 30 | "eslint": "^9.18.0", 31 | "eslint-config-next": "^15.1.4", 32 | "postcss": "^8", 33 | "tailwindcss": "^4.0.0", 34 | "typescript": "^5", 35 | "wrangler": "^4.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cli/template/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@tailwindcss/postcss": "^4.0.0", 13 | "@tanstack/react-query": "^5.51.23", 14 | "clsx": "^2.1.1", 15 | "drizzle-orm": "^0.39.0", 16 | "hono": "^4.7.0", 17 | "jstack": "^1.1.1", 18 | "next": "^15.1.6", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0", 21 | "superjson": "^2.2.1", 22 | "tailwind-merge": "^2.5.2", 23 | "zod": "^3.24.1" 24 | }, 25 | "devDependencies": { 26 | "dotenv": "^16.4.5", 27 | "@cloudflare/workers-types": "^4.20250214.0", 28 | "@types/node": "^22.10.6", 29 | "@types/react": "^19.0.7", 30 | "@types/react-dom": "^19.0.3", 31 | "drizzle-kit": "^0.30.1", 32 | "eslint": "^9.18.0", 33 | "eslint-config-next": "^15.1.4", 34 | "postcss": "^8", 35 | "tailwindcss": "^4.0.0", 36 | "typescript": "^5", 37 | "wrangler": "^4.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Josh tried coding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /www/src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | QueryCache, 5 | QueryClient, 6 | QueryClientProvider, 7 | } from "@tanstack/react-query" 8 | import { HTTPException } from "hono/http-exception" 9 | import { PropsWithChildren, useState } from "react" 10 | 11 | export const Providers = ({ children }: PropsWithChildren) => { 12 | const [queryClient] = useState( 13 | () => 14 | new QueryClient({ 15 | queryCache: new QueryCache({ 16 | onError: (err) => { 17 | let errorMessage: string 18 | if (err instanceof HTTPException) { 19 | errorMessage = err.message 20 | } else if (err instanceof Error) { 21 | errorMessage = err.message 22 | } else { 23 | errorMessage = "An unknown error occurred." 24 | } 25 | 26 | // this is where you'd display a user warning (e.g. toast notification) 27 | console.warn(errorMessage) 28 | }, 29 | }), 30 | }) 31 | ) 32 | 33 | return ( 34 | {children} 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /cli/src/utils/add-package-dep.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import fs from "fs-extra" 3 | import sortPackageJson from "sort-package-json" 4 | import { type PackageJson } from "type-fest" 5 | 6 | import { 7 | dependencyVersionMap, 8 | type AvailableDependencies, 9 | } from "@/installers/dep-version-map.js" 10 | 11 | export const addPackageDependency = (opts: { 12 | dependencies: AvailableDependencies[] 13 | devDependencies: boolean 14 | projectDir: string 15 | }) => { 16 | const { dependencies, devDependencies, projectDir } = opts 17 | 18 | const pkgJson = fs.readJSONSync( 19 | path.join(projectDir, "package.json") 20 | ) as PackageJson 21 | 22 | dependencies.forEach((pkgName) => { 23 | const version = dependencyVersionMap[pkgName] 24 | 25 | if (devDependencies && pkgJson.devDependencies) { 26 | pkgJson.devDependencies[pkgName] = version 27 | } else if (pkgJson.dependencies) { 28 | pkgJson.dependencies[pkgName] = version 29 | } 30 | }) 31 | const sortedPkgJson = sortPackageJson(pkgJson) 32 | 33 | fs.writeJSONSync(path.join(projectDir, "package.json"), sortedPkgJson, { 34 | spaces: 2, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /cli/src/helpers/scaffold-project.ts: -------------------------------------------------------------------------------- 1 | import { Dialect, Orm } from "@/cli/index.js" 2 | import { buildInstallerMap, InstallerMap, Provider } from "@/installers/index.js" 3 | import { getUserPkgManager } from "@/utils/get-user-pkg-manager.js" 4 | import path from "path" 5 | import { installBaseTemplate } from "./install-base-template.js" 6 | import { installPackages } from "./install-packages.js" 7 | 8 | interface ScaffoldProjectOptions { 9 | projectName: string 10 | orm: Orm 11 | dialect: Dialect 12 | installers: InstallerMap 13 | databaseProvider: Provider 14 | } 15 | 16 | export const scaffoldProject = async ({ databaseProvider, projectName, installers, orm }: ScaffoldProjectOptions) => { 17 | const projectDir = path.resolve(process.cwd(), projectName) 18 | const pkgManager = getUserPkgManager() 19 | 20 | await installBaseTemplate({ 21 | projectDir, 22 | pkgManager, 23 | noInstall: false, 24 | installers, 25 | projectName, 26 | databaseProvider, 27 | }) 28 | 29 | installPackages({ 30 | projectDir, 31 | pkgManager, 32 | noInstall: false, 33 | installers, 34 | projectName, 35 | databaseProvider, 36 | }) 37 | 38 | return projectDir 39 | } 40 | -------------------------------------------------------------------------------- /packages/jstack/src/server/io.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "jstack-shared" 2 | 3 | export class IO { 4 | private targetRoom: string | null = null 5 | private redisUrl: string 6 | private redisToken: string 7 | 8 | constructor(redisUrl: string, redisToken: string) { 9 | this.redisUrl = redisUrl 10 | this.redisToken = redisToken 11 | } 12 | 13 | /** 14 | * Sends to all connected clients (broadcast) 15 | */ 16 | async emit(event: K, data: OutgoingEvents[K]) { 17 | if (this.targetRoom) { 18 | await fetch(`${this.redisUrl}/publish/${this.targetRoom}`, { 19 | method: 'POST', 20 | headers: { 21 | 'Authorization': `Bearer ${this.redisToken}`, 22 | 'Content-Type': 'application/json' 23 | }, 24 | body: JSON.stringify([event, data]) 25 | }) 26 | } 27 | 28 | logger.success(`IO emitted to room "${this.targetRoom}":`, [event, data]) 29 | 30 | // Reset target room after emitting 31 | this.targetRoom = null 32 | } 33 | 34 | /** 35 | * Sends to all in a room 36 | */ 37 | to(room: string): this { 38 | this.targetRoom = room 39 | return this 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/jstack/src/server/middleware/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal middlewares 3 | * Do not modify unless you know what you're doing 4 | */ 5 | 6 | import { MiddlewareHandler } from "hono" 7 | import { parseSuperJSON } from "./utils" 8 | 9 | /** 10 | * Middleware to parse GET-request using SuperJSON 11 | */ 12 | export const queryParsingMiddleware: MiddlewareHandler = 13 | async function queryParsingMiddleware(c, next) { 14 | const rawQuery = c.req.query() 15 | const parsedQuery: Record = {} 16 | 17 | for (const [key, value] of Object.entries(rawQuery)) { 18 | parsedQuery[key] = parseSuperJSON(value) 19 | } 20 | 21 | c.set("__parsed_query", parsedQuery) 22 | await next() 23 | } 24 | 25 | /** 26 | * Middleware to parse POST-requests using SuperJSON 27 | */ 28 | export const bodyParsingMiddleware: MiddlewareHandler = 29 | async function bodyParsingMiddleware(c, next) { 30 | const rawBody = await c.req.json() 31 | const parsedBody: Record = {} 32 | 33 | for (const [key, value] of Object.entries(rawBody)) { 34 | parsedBody[key] = parseSuperJSON(value as any) 35 | } 36 | 37 | c.set("__parsed_body", parsedBody) 38 | await next() 39 | } 40 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-jstack-app", 3 | "version": "0.2.8", 4 | "description": "CLI tool to create a new JStack application", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "exports": "./dist/index.js", 8 | "files": [ 9 | "dist", 10 | "template", 11 | "package.json" 12 | ], 13 | "bin": { 14 | "create-jstack-app": "./dist/index.js" 15 | }, 16 | "scripts": { 17 | "build": "tsup", 18 | "dev": "tsup --watch", 19 | "start": "node dist/index.js", 20 | "copy-base-template": "tsx scripts/copy-base-template.ts" 21 | }, 22 | "keywords": [], 23 | "author": "Josh tried coding", 24 | "license": "ISC", 25 | "dependencies": { 26 | "@clack/prompts": "^0.8.1", 27 | "chalk": "^5.3.0", 28 | "execa": "^9.5.2", 29 | "fs-extra": "^11.2.0", 30 | "hono": "^4.7.0", 31 | "jstack": "^1.1.1", 32 | "ora": "^8.1.1", 33 | "sort-package-json": "^2.11.0", 34 | "tsup": "^8.3.5" 35 | }, 36 | "devDependencies": { 37 | "@types/fs-extra": "^11.0.4", 38 | "@types/node": "^22.9.1", 39 | "boxen": "^8.0.1", 40 | "dotenv": "^16.4.7", 41 | "drizzle-kit": "^0.30.1", 42 | "postgres": "^3.4.5", 43 | "type-fest": "^4.27.0", 44 | "typescript": "^5.6.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cli/src/installers/neon.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "path" 3 | 4 | import { PKG_ROOT } from "@/constants.js" 5 | import { addPackageDependency } from "@/utils/add-package-dep.js" 6 | import { Installer } from "./index.js" 7 | 8 | export const neonInstaller: Installer = ({ projectDir }) => { 9 | const extrasDir = path.join(PKG_ROOT, "template/extras") 10 | 11 | addPackageDependency({ 12 | projectDir, 13 | dependencies: ["@neondatabase/serverless"], 14 | devDependencies: false, 15 | }) 16 | 17 | const configFile = path.join(extrasDir, `config/drizzle-config-neon.ts`) 18 | const configDest = path.join(projectDir, "drizzle.config.ts") 19 | 20 | const schemaSrc = path.join(extrasDir, "src/server/db/schema", `with-postgres.ts`) 21 | const schemaDest = path.join(projectDir, "src/server/db/schema.ts") 22 | 23 | const jstackSrc = path.join(extrasDir, "src/server/jstack", `drizzle-with-neon.ts`) 24 | const jstackDest = path.join(projectDir, "src/server/jstack.ts") 25 | 26 | fs.ensureDirSync(path.dirname(configDest)) 27 | fs.ensureDirSync(path.dirname(schemaDest)) 28 | fs.ensureDirSync(path.dirname(jstackDest)) 29 | 30 | fs.copySync(configFile, configDest) 31 | fs.copySync(schemaSrc, schemaDest) 32 | fs.copySync(jstackSrc, jstackDest) 33 | } 34 | -------------------------------------------------------------------------------- /packages/jstack-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack-shared", 3 | "version": "0.0.4", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "scripts": { 10 | "build": "tsup" 11 | }, 12 | "exports": { 13 | "./package.json": "./package.json", 14 | ".": { 15 | "import": { 16 | "types": "./dist/index.d.ts", 17 | "default": "./dist/index.js" 18 | }, 19 | "require": { 20 | "types": "./dist/index.d.ts", 21 | "default": "./dist/index.js" 22 | } 23 | } 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "files": [ 29 | "dist", 30 | "package.json" 31 | ], 32 | "keywords": [], 33 | "author": "Josh tried coding", 34 | "license": "ISC", 35 | "devDependencies": { 36 | "@cloudflare/workers-types": "^4.20241218.0", 37 | "@types/node": "^22.10.2", 38 | "dotenv": "^16.4.7", 39 | "hono": "^4.6.17", 40 | "superjson": "^2.2.2", 41 | "ts-node": "^10.9.2", 42 | "tsup": "^8.3.5", 43 | "typescript": "^5.7.2", 44 | "zod": "^3.24.1" 45 | }, 46 | "peerDependencies": { 47 | "zod": ">=3.24.1", 48 | "hono": ">=4.6.17", 49 | "react": ">=16.8.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cli/src/helpers/install-packages.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import ora from "ora" 3 | 4 | import { type InstallerOptions, type InstallerMap } from "@/installers/index.js" 5 | import { logger } from "@/utils/logger.js" 6 | 7 | type InstallPackagesOptions = InstallerOptions 8 | 9 | // This runs the installer for all the packages that the user has selected 10 | export const installPackages = (options: InstallPackagesOptions) => { 11 | const { installers } = options 12 | logger.info("Adding boilerplate...") 13 | 14 | // Handle ORM installers 15 | for (const [name, pkgOpts] of Object.entries(installers.orm)) { 16 | if (pkgOpts.inUse) { 17 | const spinner = ora(`Boilerplating ORM: ${name}...`).start() 18 | pkgOpts.installer(options) 19 | spinner.succeed(chalk.green(`Successfully setup boilerplate for ORM: ${chalk.green.bold(name)}`)) 20 | } 21 | } 22 | 23 | // Handle provider installers 24 | for (const [name, pkgOpts] of Object.entries(installers.provider)) { 25 | if (pkgOpts.inUse) { 26 | const spinner = ora(`Boilerplating provider: ${name}...`).start() 27 | pkgOpts.installer(options) 28 | spinner.succeed(chalk.green(`Successfully setup boilerplate for provider: ${chalk.green.bold(name)}`)) 29 | } 30 | } 31 | 32 | logger.info("") 33 | } 34 | -------------------------------------------------------------------------------- /www/src/components/shiny-button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import Link from "next/link" 3 | import React from "react" 4 | 5 | export interface ShinyButtonProps extends React.AnchorHTMLAttributes { 6 | href: string 7 | loading?: boolean 8 | loadingColor?: "gray" | "white" 9 | } 10 | 11 | export const ShinyButton = ({ 12 | className, 13 | children, 14 | href, 15 | loading = false, 16 | loadingColor = "white", 17 | ...props 18 | }: ShinyButtonProps) => { 19 | return ( 20 | 30 | {children} 31 | 32 |
33 | 34 | ) 35 | } 36 | 37 | ShinyButton.displayName = "ShinyButton" 38 | -------------------------------------------------------------------------------- /cli/src/installers/planetscale.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "path" 3 | 4 | import { PKG_ROOT } from "@/constants.js" 5 | import { addPackageDependency } from "@/utils/add-package-dep.js" 6 | import { Installer } from "./index.js" 7 | 8 | export const planetscaleInstaller: Installer = ({ projectDir }) => { 9 | const extrasDir = path.join(PKG_ROOT, "template/extras") 10 | 11 | addPackageDependency({ 12 | projectDir, 13 | dependencies: ["@planetscale/database"], 14 | devDependencies: false, 15 | }) 16 | 17 | const configFile = path.join(extrasDir, `config/drizzle-config-planetscale.ts`) 18 | const configDest = path.join(projectDir, "drizzle.config.ts") 19 | 20 | const schemaSrc = path.join(extrasDir, "src/server/db/drizzle", `with-mysql.ts`) 21 | const schemaDest = path.join(projectDir, "src/server/db/schema.ts") 22 | 23 | const jstackSrc = path.join(extrasDir, "src/server/jstack", `drizzle-with-planetscale.ts`) 24 | const jstackDest = path.join(projectDir, "src/server/jstack.ts") 25 | 26 | fs.ensureDirSync(path.dirname(configDest)) 27 | fs.ensureDirSync(path.dirname(schemaDest)) 28 | fs.ensureDirSync(path.dirname(jstackDest)) 29 | 30 | fs.copySync(configFile, configDest) 31 | fs.copySync(schemaSrc, schemaDest) 32 | fs.copySync(jstackSrc, jstackDest) 33 | } 34 | -------------------------------------------------------------------------------- /cli/src/installers/vercel-postgres.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "path" 3 | 4 | import { PKG_ROOT } from "@/constants.js" 5 | import { addPackageDependency } from "@/utils/add-package-dep.js" 6 | import { Installer } from "./index.js" 7 | 8 | export const vercelPostgresInstaller: Installer = ({ projectDir }) => { 9 | const extrasDir = path.join(PKG_ROOT, "template/extras") 10 | 11 | addPackageDependency({ 12 | projectDir, 13 | dependencies: ["@vercel/postgres"], 14 | devDependencies: false, 15 | }) 16 | 17 | const configFile = path.join(extrasDir, `config/drizzle-config-vercel-postgres.ts`) 18 | const configDest = path.join(projectDir, "drizzle.config.ts") 19 | 20 | const schemaSrc = path.join(extrasDir, "src/server/db/schema", `with-postgres.ts`) 21 | const schemaDest = path.join(projectDir, "src/server/db/schema.ts") 22 | 23 | const jstackSrc = path.join(extrasDir, "src/server/jstack", `drizzle-with-vercel-postgres.ts`) 24 | const jstackDest = path.join(projectDir, "src/server/jstack.ts") 25 | 26 | fs.ensureDirSync(path.dirname(configDest)) 27 | fs.ensureDirSync(path.dirname(schemaDest)) 28 | fs.ensureDirSync(path.dirname(jstackDest)) 29 | 30 | fs.copySync(configFile, configDest) 31 | fs.copySync(schemaSrc, schemaDest) 32 | fs.copySync(jstackSrc, jstackDest) 33 | } 34 | -------------------------------------------------------------------------------- /www/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { cn, constructMetadata } from "@/lib/utils" 2 | import type { Metadata, Viewport } from "next" 3 | import { Inter, Noto_Sans } from "next/font/google" 4 | import localFont from "next/font/local" 5 | import { Providers } from "../components/providers" 6 | 7 | const fontCode = localFont({ 8 | src: "../assets/JetBrainsMonoNL-Medium.ttf", 9 | variable: "--font-code", 10 | }) 11 | 12 | import "./globals.css" 13 | 14 | const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }) 15 | 16 | const noto_sans = Noto_Sans({ subsets: ["latin"], variable: "--font-noto" }) 17 | 18 | export const viewport: Viewport = { 19 | maximumScale: 1, // Disable auto-zoom on mobile Safari, credit to https://github.com/ai-ng 20 | } 21 | 22 | export const metadata: Metadata = constructMetadata() 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode 28 | }>) { 29 | return ( 30 | 39 | 40 | {children} 41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /cli/src/installers/postgres.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "path" 3 | 4 | import { PKG_ROOT } from "@/constants.js" 5 | import { addPackageDependency } from "@/utils/add-package-dep.js" 6 | import { logger } from "@/utils/logger.js" 7 | import { Installer } from "./index.js" 8 | 9 | export const postgresInstaller: Installer = ({ projectDir }) => { 10 | const extrasDir = path.join(PKG_ROOT, "template/extras") 11 | logger.info("Installing Postgres...") 12 | 13 | addPackageDependency({ 14 | projectDir, 15 | dependencies: ["postgres"], 16 | devDependencies: false, 17 | }) 18 | 19 | const configFile = path.join(extrasDir, `config/drizzle-config-postgres.ts`) 20 | const configDest = path.join(projectDir, "drizzle.config.ts") 21 | 22 | const schemaSrc = path.join(extrasDir, "src/server/db/schema", `with-postgres.ts`) 23 | const schemaDest = path.join(projectDir, "src/server/db/schema.ts") 24 | 25 | const jstackSrc = path.join(extrasDir, "src/server/jstack", `drizzle-with-postgres.ts`) 26 | const jstackDest = path.join(projectDir, "src/server/jstack.ts") 27 | 28 | fs.ensureDirSync(path.dirname(configDest)) 29 | fs.ensureDirSync(path.dirname(schemaDest)) 30 | fs.ensureDirSync(path.dirname(jstackDest)) 31 | 32 | fs.copySync(configFile, configDest) 33 | fs.copySync(schemaSrc, schemaDest) 34 | fs.copySync(jstackSrc, jstackDest) 35 | } 36 | -------------------------------------------------------------------------------- /www/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 30 | 31 | )) 32 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 33 | 34 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 35 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next linta" 10 | }, 11 | "dependencies": { 12 | "@hono/zod-validator": "^0.4.1", 13 | "@neondatabase/serverless": "^0.10.3", 14 | "@tailwindcss/postcss": "^4.0.0", 15 | "@tanstack/react-query": "^5.51.23", 16 | "@upstash/redis": "^1.34.3", 17 | "clsx": "^2.1.1", 18 | "dotenv": "^16.4.5", 19 | "drizzle-orm": "^0.39.0", 20 | "hono": "^4.6.19", 21 | "jstack": "^1.1.1", 22 | "next": "^15.1.6", 23 | "nuqs": "^2.2.3", 24 | "postgres": "^3.4.5", 25 | "react": "^19.0.0", 26 | "react-dom": "^19.0.0", 27 | "superjson": "^2.2.1", 28 | "tailwind-merge": "^2.5.2", 29 | "wrangler": "^3.72.0", 30 | "ws": "^8.18.0", 31 | "zod": "3.24.1" 32 | }, 33 | "devDependencies": { 34 | "@cloudflare/workers-types": "^4.20240815.0", 35 | "@types/node": "^22.10.6", 36 | "@types/react": "^19.0.7", 37 | "@types/react-dom": "^19.0.3", 38 | "@types/ws": "^8.5.13", 39 | "boxen": "^8.0.1", 40 | "drizzle-kit": "^0.30.1", 41 | "eslint": "^9.18.0", 42 | "eslint-config-next": "^15.1.4", 43 | "ora": "^8.1.1", 44 | "postcss": "^8", 45 | "tailwindcss": "^4.0.0", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { RecentPost } from "./components/post" 3 | 4 | export default async function Home() { 5 | return ( 6 |
7 |
8 |
9 |

17 | JStack 18 |

19 | 20 |

21 | The stack for building seriously fast, lightweight and{" "} 22 | 23 | end-to-end typesafe Next.js apps. 24 | 25 |

26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /cli/template/base/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { RecentPost } from "./components/post" 3 | 4 | export default async function Home() { 5 | return ( 6 |
7 |
8 |
9 |

17 | JStack 18 |

19 | 20 |

21 | The stack for building seriously fast, lightweight and{" "} 22 | 23 | end-to-end typesafe Next.js apps. 24 | 25 |

26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/jstack/src/client/hooks/use-web-socket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | import { ClientSocket, SystemEvents } from "jstack-shared" 3 | 4 | export function useWebSocket< 5 | IncomingEvents extends Partial & Record, 6 | >( 7 | socket: ClientSocket, 8 | events: Partial<{ 9 | [K in keyof (IncomingEvents & SystemEvents)]: ( 10 | data: (IncomingEvents & SystemEvents)[K] 11 | ) => void 12 | }>, 13 | opts: { enabled?: boolean } = { enabled: true } 14 | ) { 15 | const eventsRef = useRef(events) 16 | eventsRef.current = events 17 | 18 | useEffect(() => { 19 | if (opts?.enabled === false) { 20 | return 21 | } 22 | 23 | const defaultHandlers = { 24 | onConnect: () => {}, 25 | onError: () => {}, 26 | } 27 | 28 | const mergedEvents = { 29 | ...defaultHandlers, 30 | ...events, 31 | } 32 | 33 | const eventNames = Object.keys(mergedEvents) as Array< 34 | keyof IncomingEvents & SystemEvents 35 | > 36 | 37 | eventNames.forEach((eventName) => { 38 | const handler = mergedEvents[eventName] 39 | 40 | if (handler) { 41 | socket.on(eventName, handler) 42 | } 43 | }) 44 | 45 | return () => { 46 | eventNames.forEach((eventName) => { 47 | const handler = mergedEvents[eventName] 48 | socket.off(eventName, handler) 49 | }) 50 | } 51 | }, [opts?.enabled]) 52 | } 53 | -------------------------------------------------------------------------------- /www/src/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | interface CopyButtonProps { 6 | code: string 7 | className?: string 8 | } 9 | 10 | export const CopyButton = ({ code, className }: CopyButtonProps) => { 11 | const [copied, setCopied] = useState(false) 12 | 13 | const onCopy = async () => { 14 | // Extract just the code content by removing backticks and meta string 15 | const cleanCode = code.replace(/^```.*\n([\s\S]*?)```$/m, '$1').trim() 16 | await navigator.clipboard.writeText(cleanCode) 17 | setCopied(true) 18 | setTimeout(() => setCopied(false), 2000) 19 | } 20 | 21 | return ( 22 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /cli/src/index.ts: -------------------------------------------------------------------------------- 1 | // #!/usr/bin/env node 2 | 3 | import fs from "fs-extra" 4 | import path from "path" 5 | 6 | import { runCli } from "./cli/index.js" 7 | import { scaffoldProject } from "./helpers/scaffold-project.js" 8 | import { buildInstallerMap } from "./installers/index.js" 9 | import { logger } from "./utils/logger.js" 10 | import { installDependencies } from "./helpers/install-deps.js" 11 | 12 | const main = async () => { 13 | const results = await runCli() 14 | 15 | if (!results) { 16 | return 17 | } 18 | 19 | const { projectName, orm, dialect, provider } = results 20 | 21 | const installers = buildInstallerMap(orm, provider) 22 | 23 | const projectDir = await scaffoldProject({ 24 | orm, 25 | dialect, 26 | databaseProvider: provider ?? "neon", 27 | installers, 28 | projectName, 29 | }) 30 | 31 | const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) 32 | pkgJson.name = projectName 33 | 34 | fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { 35 | spaces: 2, 36 | }) 37 | 38 | if (!results.noInstall) { 39 | await installDependencies({ projectDir }) 40 | } 41 | 42 | process.exit(0) 43 | } 44 | 45 | main().catch((err) => { 46 | logger.error("Aborting installation...") 47 | if (err instanceof Error) { 48 | logger.error(err) 49 | } else { 50 | logger.error( 51 | "An unknown error has occurred. Please open an issue on github with the below:" 52 | ) 53 | console.log(err) 54 | } 55 | process.exit(1) 56 | }) 57 | -------------------------------------------------------------------------------- /www/content-collections.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, defineConfig } from "@content-collections/core" 2 | import { compileMDX } from "@content-collections/mdx" 3 | import rehypeAutolinkHeadings from "rehype-autolink-headings" 4 | import { rehypeParseCodeBlocks } from "./shiki-rehype.mjs" 5 | import { slugify } from "@/lib/utils" 6 | 7 | const docs = defineCollection({ 8 | name: "docs", 9 | directory: "src/docs", 10 | include: ["**/*.md", "**/*.mdx"], 11 | schema: (z) => ({ 12 | title: z.string(), 13 | summary: z.string(), 14 | }), 15 | transform: async (document, context) => { 16 | const mdx = await compileMDX(context, document, { 17 | rehypePlugins: [ 18 | rehypeParseCodeBlocks, 19 | [ 20 | rehypeAutolinkHeadings, 21 | { 22 | properties: { 23 | className: ["anchor"], 24 | }, 25 | }, 26 | ], 27 | ], 28 | }) 29 | 30 | const regXHeader = /^(?:[\n\r]|)(?#{1,6})\s+(?.+)/gm 31 | const headings = Array.from(document.content.matchAll(regXHeader)).map( 32 | ({ groups }) => { 33 | const flag = groups?.flag 34 | const content = groups?.content 35 | return { 36 | level: flag?.length, 37 | text: content, 38 | slug: slugify(content ?? "#") 39 | } 40 | } 41 | ) 42 | 43 | return { 44 | ...document, 45 | headings, 46 | mdx, 47 | } 48 | }, 49 | }) 50 | 51 | export default defineConfig({ 52 | collections: [docs], 53 | }) 54 | -------------------------------------------------------------------------------- /www/src/docs/getting-started/local-development.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Local Development 3 | summary: Local development with JStack 4 | --- 5 | 6 | # Quickstart 7 | 8 | Start the Next.js dev server by running: 9 | 10 | ```bash Terminal 11 | npm run dev 12 | ``` 13 | 14 | This command will start your Next.js application at `http://localhost:3000`. This is all you need to do if you plan to deploy to Vercel, Netlify, or similar. 15 | 16 | --- 17 | 18 | ## Cloudflare Worker Setup 19 | 20 | If you are using JStack to develop for Cloudflare Workers, launch two terminal windows to run the frontend and backend separately: 21 | 22 | ```bash Terminal 23 | # Terminal 1: Frontend 24 | npm run dev # Available on http://localhost:3000 25 | 26 | # Terminal 2: Backend 27 | wrangler dev # Available on http://localhost:8080 28 | ``` 29 | 30 | These commands will start your: 31 | 32 | - Frontend on `http://localhost:3000` 33 | - Backend on `http://localhost:8080` 34 | 35 | To let your frontend know about your backend on a separate port, adjust your client: 36 | 37 | ```ts lib/client.ts {5-6} 38 | import type { AppRouter } from "@/server" 39 | import { createClient } from "jstack" 40 | 41 | export const client = createClient({ 42 | // 👇 Add our port 8080 cloudflare URL 43 | baseUrl: "http://localhost:8080/api", 44 | }) 45 | ``` 46 | 47 | **How it works** 48 | 49 | - Your frontend runs as a standard Next.js application 50 | - Your backend runs in Wrangler, Cloudflare's development environment 51 | - API requests from your frontend are automatically routed to the correct backend URL -------------------------------------------------------------------------------- /www/shiki-rehype.mjs: -------------------------------------------------------------------------------- 1 | import { visit } from "unist-util-visit" 2 | 3 | export function rehypeParseCodeBlocks() { 4 | return (tree) => { 5 | visit(tree, "element", (node, _nodeIndex, parentNode) => { 6 | if (node.tagName === "code" && node.properties?.className) { 7 | const language = 8 | node.properties.className[0]?.replace(/^language-/, "") || "text" 9 | const metastring = node.data?.meta || "" 10 | 11 | let title = null 12 | if (metastring) { 13 | const excludeMatch = metastring.match(/\s+\/([^/]+)\//) 14 | 15 | if (excludeMatch) { 16 | const cleanMetastring = metastring.replace(excludeMatch[0], "") 17 | const titleMatch = cleanMetastring.match(/^([^{]+)/) 18 | if (titleMatch) { 19 | title = titleMatch[1].trim() 20 | } 21 | } else { 22 | const titleMatch = metastring.match(/^([^{]+)/) 23 | if (titleMatch) { 24 | title = titleMatch[1].trim() 25 | } 26 | } 27 | } 28 | 29 | parentNode.properties = parentNode.properties || {} 30 | 31 | parentNode.properties.language = language 32 | parentNode.properties.title = title 33 | parentNode.properties.meta = metastring 34 | 35 | const codeContent = node.children[0]?.value || "" 36 | 37 | parentNode.properties.code = [ 38 | "```" + language + (metastring ? " " + metastring : ""), 39 | codeContent.trimEnd(), 40 | "```", 41 | ].join("\n") 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /www/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /packages/jstack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstack", 3 | "version": "1.1.1", 4 | "author": "Josh tried coding", 5 | "main": "dist/server/index.js", 6 | "module": "dist/server/index.mjs", 7 | "devDependencies": { 8 | "@cloudflare/workers-types": "^4.20241218.0", 9 | "@types/node": "^22.10.2", 10 | "chalk": "^5.4.0", 11 | "dotenv": "^16.4.7", 12 | "superjson": "^2.2.2", 13 | "ts-node": "^10.9.2", 14 | "tsup": "^8.3.5", 15 | "typescript": "^5.7.2" 16 | }, 17 | "peerDependencies": { 18 | "zod": ">=3.24.1", 19 | "hono": ">=4.6.17", 20 | "react": ">=16.8.0" 21 | }, 22 | "exports": { 23 | "./package.json": "./package.json", 24 | ".": { 25 | "import": { 26 | "types": "./dist/server/index.d.ts", 27 | "default": "./dist/server/index.js" 28 | }, 29 | "require": { 30 | "types": "./dist/server/index.d.ts", 31 | "default": "./dist/server/index.js" 32 | } 33 | }, 34 | "./client": { 35 | "import": { 36 | "types": "./dist/client/index.d.ts", 37 | "default": "./dist/client/index.js" 38 | }, 39 | "require": { 40 | "types": "./dist/client/index.d.ts", 41 | "default": "./dist/client/index.js" 42 | } 43 | } 44 | }, 45 | "description": "", 46 | "files": [ 47 | "dist", 48 | "package.json" 49 | ], 50 | "keywords": [], 51 | "license": "ISC", 52 | "publishConfig": { 53 | "access": "public" 54 | }, 55 | "scripts": { 56 | "build": "tsup" 57 | }, 58 | "type": "module", 59 | "types": "dist/server/index.d.ts", 60 | "dependencies": { 61 | "jstack-shared": "^0.0.4" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /www/src/server/jstack.ts: -------------------------------------------------------------------------------- 1 | import { env } from "hono/adapter" 2 | import { jstack } from "jstack" 3 | import { Redis } from "@upstash/redis" 4 | import { Client } from "@upstash/qstash" 5 | import { Index } from "@upstash/vector" 6 | 7 | interface Env { 8 | Bindings: { 9 | GITHUB_TOKEN: string 10 | QSTASH_TOKEN: string 11 | AMPLIFY_URL: string 12 | QSTASH_NEXT_SIGNING_KEY: string 13 | QSTASH_CURRENT_SIGNING_KEY: string 14 | UPSTASH_REDIS_REST_URL: string 15 | UPSTASH_REDIS_REST_TOKEN: string 16 | UPSTASH_VECTOR_REST_URL: string 17 | UPSTASH_VECTOR_REST_TOKEN: string 18 | } 19 | } 20 | 21 | export const j = jstack.init() 22 | 23 | const redisMiddleware = j.middleware(async ({ c, next }) => { 24 | const { QSTASH_TOKEN, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN } = 25 | env(c) 26 | 27 | const redis = new Redis({ 28 | url: UPSTASH_REDIS_REST_URL, 29 | token: UPSTASH_REDIS_REST_TOKEN, 30 | }) 31 | 32 | const qstash = new Client({ token: QSTASH_TOKEN }) 33 | 34 | return await next({ redis, qstash }) 35 | }) 36 | 37 | export const vectorMiddleware = j.middleware(async ({ c, next }) => { 38 | const { UPSTASH_VECTOR_REST_URL, UPSTASH_VECTOR_REST_TOKEN } = env(c) 39 | 40 | const index = new Index({ 41 | url: UPSTASH_VECTOR_REST_URL, 42 | token: UPSTASH_VECTOR_REST_TOKEN, 43 | }) 44 | 45 | return await next({ index }) 46 | }) 47 | 48 | /** 49 | * Public (unauthenticated) procedures 50 | * 51 | * This is the base piece you use to build new queries and mutations on your API. 52 | */ 53 | export const baseProcedure = j.procedure 54 | export const publicProcedure = baseProcedure.use(redisMiddleware) 55 | -------------------------------------------------------------------------------- /www/src/components/active-section-observer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTableOfContents } from "@/ctx/use-table-of-contents" 4 | import { useEffect } from "react" 5 | 6 | interface ActiveSectionObserverProps { 7 | children: React.ReactNode 8 | headings: Array<{ level: number; text: string }> 9 | } 10 | 11 | export function ActiveSectionObserver({ children, headings }: ActiveSectionObserverProps) { 12 | const setVisibleSections = useTableOfContents((state) => state.setVisibleSections) 13 | const setAllHeadings = useTableOfContents((state) => state.setAllHeadings) 14 | 15 | useEffect(() => { 16 | setAllHeadings(headings.filter(({ level }) => level === 2 || level === 3)) 17 | }, [headings, setAllHeadings]) 18 | 19 | useEffect(() => { 20 | const observers: IntersectionObserver[] = [] 21 | const headingElements = document.querySelectorAll("h1, h2") 22 | 23 | const callback: IntersectionObserverCallback = (entries) => { 24 | const visibleSections = entries.filter((entry) => entry.isIntersecting).map((entry) => entry.target.id) 25 | 26 | if (visibleSections.length === 0) { 27 | // if no section is visible, we might want to show the closest one 28 | // but i dont care right now 29 | return 30 | } 31 | 32 | setVisibleSections(visibleSections) 33 | } 34 | 35 | const observer = new IntersectionObserver(callback, { 36 | rootMargin: "-100px 0px -66%", 37 | threshold: 1, 38 | }) 39 | 40 | headingElements.forEach((element) => { 41 | observer.observe(element) 42 | }) 43 | 44 | return () => { 45 | headingElements.forEach((element) => { 46 | observer.unobserve(element) 47 | }) 48 | } 49 | }, [setVisibleSections]) 50 | 51 | return children 52 | } 53 | -------------------------------------------------------------------------------- /packages/jstack/src/server/dynamic.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "./router" 2 | 3 | /** 4 | * Dynamically imports routers to enable code splitting. Improves performance and reduces cold starts. 5 | * 6 | * @see https://jstack.app/docs/backend/performance 7 | * 8 | * @param importFn - Function that imports a router 9 | * @throws {Error} If module has no exports, multiple exports, or invalid Router 10 | * 11 | * @example 12 | * import { dynamic } from "jstack" 13 | * 14 | * const appRouter = j.mergeRouters(api, { 15 | * router: dynamic(() => import("./routers/my-router")), 16 | * }) 17 | */ 18 | export const dynamic = ( 19 | importFn: () => Promise<{ [key: string]: T }>, 20 | ) => { 21 | return async () => { 22 | const module = await importFn() 23 | const routers = Object.values(module) 24 | 25 | if (routers.length === 0) { 26 | throw new Error( 27 | "Error dynamically loading router: Invalid router module - Expected a default or named export of a Router, but received an empty module. Did you forget to export your router?", 28 | ) 29 | } 30 | 31 | if (routers.length > 1) { 32 | throw new Error( 33 | `Error dynamically loading router: Multiple Router exports detected in module (${Object.keys(module).join(", ")}). ` + 34 | "Please export only one Router instance per module." 35 | ) 36 | } 37 | 38 | const router = routers[0] 39 | if (!(router instanceof Router)) { 40 | throw new Error( 41 | "Error dynamically loading router: Invalid router module - Expected exported value to be a Router instance, " + 42 | `but received ${router === null ? "null" : typeof router}. Are you exporting multiple functions from this file?`, 43 | ) 44 | } 45 | 46 | return router 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cli/src/installers/drizzle.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "path" 3 | import { type PackageJson } from "type-fest" 4 | 5 | import { PKG_ROOT } from "@/constants.js" 6 | import { type Installer } from "@/installers/index.js" 7 | import { addPackageDependency } from "@/utils/add-package-dep.js" 8 | 9 | export const drizzleInstaller: Installer = ({ projectDir, databaseProvider }) => { 10 | addPackageDependency({ 11 | projectDir, 12 | dependencies: ["drizzle-kit", "eslint-plugin-drizzle"], 13 | devDependencies: true, 14 | }) 15 | addPackageDependency({ 16 | projectDir, 17 | dependencies: ["drizzle-orm"], 18 | devDependencies: false, 19 | }) 20 | 21 | const extrasDir = path.join(PKG_ROOT, "template/extras") 22 | 23 | const routerSrc = path.join(extrasDir, `src/server/routers/post/with-drizzle.ts`) 24 | const routerDest = path.join(projectDir, `src/server/routers/post-router.ts`) 25 | 26 | const envSrc = path.join(extrasDir, `config/_env-drizzle`) 27 | const vercelPostgresEnvSrc = path.join(extrasDir, `config/_env-drizzle-vercel-postgres`) 28 | 29 | const envDest = path.join(projectDir, ".env") 30 | 31 | // add db:* scripts to package.json 32 | const packageJsonPath = path.join(projectDir, "package.json") 33 | 34 | const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson 35 | packageJsonContent.scripts = { 36 | ...packageJsonContent.scripts, 37 | "db:push": "drizzle-kit push", 38 | "db:studio": "drizzle-kit studio", 39 | "db:generate": "drizzle-kit generate", 40 | "db:migrate": "drizzle-kit migrate", 41 | } 42 | 43 | fs.copySync(routerSrc, routerDest) 44 | fs.copySync(databaseProvider === "vercel-postgres" ? vercelPostgresEnvSrc : envSrc, envDest) 45 | 46 | fs.writeJSONSync(packageJsonPath, packageJsonContent, { 47 | spaces: 2, 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /www/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /packages/jstack/src/server/merge-routers.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono" 2 | import { Router } from "./router" 3 | 4 | export type InferSchemaFromRouters< 5 | R extends Record | (() => Promise>)>, 6 | > = { 7 | [P in keyof R]: R[P] extends () => Promise> 8 | ? R[P] extends () => Promise 9 | ? T extends Hono 10 | ? { [Q in keyof S]: S[Q] } 11 | : never 12 | : never 13 | : R[P] extends Hono 14 | ? { [Q in keyof S]: S[Q] } 15 | : never 16 | } 17 | 18 | export function mergeRouters< 19 | R extends Record | (() => Promise>)>, 20 | >(api: Hono, routers: R): Router> { 21 | const mergedRouter = new Router() 22 | Object.assign(mergedRouter, api) 23 | 24 | mergedRouter._metadata = { 25 | subRouters: {}, 26 | config: {}, 27 | procedures: {}, 28 | registeredPaths: [], 29 | } 30 | 31 | for (const [key, router] of Object.entries(routers)) { 32 | // lazy-loaded routers using `dynamic()` use proxy to avoid loading bundle initially 33 | if (typeof router === "function") { 34 | const proxyRouter = new Router() 35 | 36 | proxyRouter.all("*", async (c) => { 37 | const actualRouter = await router() 38 | mergedRouter._metadata.subRouters[`/api/${key}`] = actualRouter 39 | 40 | return actualRouter.fetch(c.req.raw, c.env) 41 | }) 42 | mergedRouter._metadata.subRouters[`/api/${key}`] = proxyRouter 43 | } else if (router instanceof Router) { 44 | // statically imported routers can be assigned directly 45 | mergedRouter._metadata.subRouters[`/api/${key}`] = router 46 | } 47 | } 48 | 49 | mergedRouter.registerSubrouterMiddleware() 50 | 51 | return mergedRouter as Router> 52 | } 53 | -------------------------------------------------------------------------------- /www/src/components/landing/stargazer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Avatar, AvatarImage } from "@/components/ui/avatar" 4 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" 5 | import { cn } from "@/lib/utils" 6 | 7 | export function Stargazer({ login, name }: { login: string; name: string }) { 8 | return ( 9 | 10 | 11 | 12 |

{name}

13 |

@{login}

14 |
15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | ) 30 | } 31 | 32 | export function StargazerMore() { 33 | return ( 34 |
35 | 36 |

99+

37 |
38 |
39 | ) 40 | } 41 | 42 | export function StargazerLoading() { 43 | return ( 44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /www/src/components/landing/lambo-section.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import { Icons } from "../icons" 3 | 4 | export const LamboSection = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 | JStack Next.js TypeScript stack for Vercel, AWS, Netlify and more 17 |
18 |
19 | 20 |
21 |
22 |

23 | "JStack is built on top of some of the highest quality web software. It's the framework I wish 24 | I'd had when I started — crafted from years of experience building production Next.js apps. Lambos 25 | are not just for PHP devs anymore :^)" 26 |

27 |
28 | 29 |
30 | 31 |
32 |

Josh

33 |

Creator of JStack (and doesnt even own a car)

34 |
35 |
36 |
37 |
38 |
39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /cli/scripts/copy-base-template.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra" 2 | import path from "node:path" 3 | 4 | const filesToIgnore = [ 5 | "src/server/jstack.ts", 6 | "src/server/routers/post-router.ts", 7 | "drizzle.config.ts", 8 | "node_modules", 9 | ".git", 10 | ".vscode", 11 | ".wrangler", 12 | ".dev.vars", 13 | ".next", 14 | ".turbo", 15 | "dist", 16 | "eslintrc.json", 17 | ".DS_Store", 18 | "bun.lockb", 19 | ".env", 20 | ".prettierrc" 21 | ] 22 | 23 | async function main() { 24 | const sourceDir = path.join(process.cwd(), "..", "app") 25 | const targetDir = path.join(process.cwd(), "template", "base") 26 | 27 | try { 28 | // Ensure source directory exists 29 | if (!(await fs.pathExists(sourceDir))) { 30 | throw new Error(`Source directory not found at: ${sourceDir}`) 31 | } 32 | 33 | // Remove target directory if it exists 34 | await fs.remove(targetDir) 35 | 36 | // Copy app directory to template/base, excluding ignored paths 37 | await fs.copy(sourceDir, targetDir, { 38 | filter: (src) => { 39 | const relativePath = path.relative(sourceDir, src) 40 | return !filesToIgnore.some((path) => 41 | // Check if the path contains the ignored file/folder 42 | relativePath.includes(path) || 43 | // Check if any parent directory matches exactly 44 | relativePath.split("/").some(part => path === part) 45 | ) 46 | }, 47 | }) 48 | 49 | // Replace package.json with base template version 50 | const basePackageJson = path.join(process.cwd(), "template/base-assets/base-package.json") 51 | const targetPackageJson = path.join(targetDir, "package.json") 52 | await fs.copy(basePackageJson, targetPackageJson) 53 | console.log("Replaced package.json with base template version") 54 | 55 | console.log("Successfully copied base template!") 56 | } catch (error) { 57 | console.error("Error copying base template:", error) 58 | process.exit(1) 59 | } 60 | } 61 | 62 | main() 63 | -------------------------------------------------------------------------------- /www/src/docs/backend/performance.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Performance 3 | summary: Optimizing JStack Performance 4 | --- 5 | 6 | # Performance 7 | 8 | JStack is built on lightweight software such as Hono and Drizzle ORM for high performance out of the box. 9 | 10 | To further optimize your app, JStack allows you to **optimize bundle sizes and significantly reduce cold starts** through built-in dynamic router and dependency loading. 11 | 12 | --- 13 | 14 | ## Dynamically Loading Routers 15 | 16 | Dynamically load routers using the `dynamic()` function to maintain high performance when scaling to many routers. This approach reduces the initial bundle size & improves cold starts: 17 | 18 | ```ts server/index.ts {2,11-12} 19 | import { j } from "./jstack" 20 | import { dynamic } from "jstack" 21 | 22 | const api = j 23 | .router() 24 | .basePath("/api") 25 | .use(j.defaults.cors) 26 | .onError(j.defaults.errorHandler) 27 | 28 | const appRouter = j.mergeRouters(api, { 29 | users: dynamic(() => import("./routers/user-router")), 30 | posts: dynamic(() => import("./routers/post-router")), 31 | }) 32 | 33 | export type AppRouter = typeof appRouter 34 | export default appRouter 35 | ``` 36 | 37 | --- 38 | 39 | ## Dynamic Imports in Procedures 40 | 41 | JStack also supports dynamic imports within procedures for code splitting at the procedure level. Just like dynamically loading routers, this approach reduces the initial bundle size and can significantly improve cold starts: 42 | 43 | ```ts server/routers/user-router.ts {6-7} 44 | import { j, publicProcedure } from "../jstack" 45 | 46 | export const userRouter = j.router({ 47 | generateReport: publicProcedure.get(async ({ c }) => { 48 | // 👇 Dynamically import heavy dependencies 49 | const { generatePDF } = await import("./utils/pdf-generator") 50 | const { processData } = await import("./utils/data-processor") 51 | 52 | const data = await processData(c.req.query()) 53 | const pdf = await generatePDF(data) 54 | 55 | return c.json({ pdf }) 56 | }) 57 | }) 58 | ``` 59 | -------------------------------------------------------------------------------- /www/src/docs/introduction/key-features.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Key Features 3 | summary: This is my first post 4 | --- 5 | 6 | # Key Features 7 | 8 | ## 1. Ultra fast & Lightweight 9 | 10 | Built on top of Hono's lightweight [RegExpRouter](https://hono.dev/docs/concepts/routers#regexprouter) and [Drizzle ORM](https://orm.drizzle.team/), which, unlike Prisma, does not ship a massive query engine, JStack is fast. With a single click, you can deploy your Next.js server to globally distributed Cloudflare workers for maximum performance. 11 | 12 | You can follow the Vercel pattern (coming soon) to get the same performance on Vercel, Netlify, Railway and other deployment platforms. 13 | 14 | --- 15 | 16 | ## 2. End-to-end Type-Safe 17 | 18 | Type-safety is my favorite part about the [T3 Stack](https://create.t3.gg/). The difference in JStack is that its client is not coupled to React Query hooks. In JStack you get a simple TypeScript client that you can use with any state manager you want: 19 | 20 | ```tsx app/page.tsx 21 | const res = await client.post.recent.$get() 22 | const post = await res.json() 23 | // ^ automatically type-safe: { post: Post } 24 | ``` 25 | 26 | Learn how to set up your lightweight, type-safe client in our [first steps](/docs/getting-started/first-steps). 27 | 28 | --- 29 | 30 | ## 3. Highly Cost Effective 31 | 32 | If you follow the _"intended way"_ of deploying Next.js, you'll deploy your app to Vercel, Netlify, or similar and call it a day. With JStack, you can deploy your backend to Cloudflare Workers (or any other platform) with a single click, completely separate from your frontend. 33 | 34 | Cloudflare Workers are also one of the **cheapest** (100K requests per day for free, no credit card required) and **fastest** ways to run your serverless code. 35 | 36 | ```bash Terminal 37 | npx wrangler deploy server/index.ts 38 | ``` 39 | 40 | I have run entire software projects on Cloudflare's free plan, such as [profanity.dev](https://profanity.dev). I think Cloudflare Workers is the most underrated computing platform in 2025 (this is not sponsored in _any_ way). 41 | 42 | -------------------------------------------------------------------------------- /www/src/scripts/index-docs.ts: -------------------------------------------------------------------------------- 1 | import { slugify } from "@/lib/utils" 2 | import { SearchMetadata } from "@/types" 3 | import { Index } from "@upstash/vector" 4 | import { allDocs } from "content-collections" 5 | import "dotenv/config" 6 | 7 | const index = new Index({ 8 | url: process.env.UPSTASH_VECTOR_REST_URL!, 9 | token: process.env.UPSTASH_VECTOR_REST_TOKEN!, 10 | }) 11 | 12 | function splitMdxByHeadings(mdx: string) { 13 | const sections = mdx.split(/(?=^#{1,6}\s)/m) 14 | 15 | return sections 16 | .map((section) => { 17 | const lines = section.trim().split("\n") 18 | const headingMatch = lines[0]?.match(/^(#{1,6})\s+(.+)$/) 19 | 20 | if (!headingMatch) return null 21 | 22 | const [, hashes, title] = headingMatch 23 | const content = lines.slice(1).join("\n").trim() 24 | 25 | return { 26 | level: hashes?.length, 27 | title, 28 | content, 29 | } 30 | }) 31 | .filter(Boolean) 32 | } 33 | 34 | async function indexDocs() { 35 | await index.reset() 36 | 37 | for (const doc of allDocs) { 38 | try { 39 | const sections = splitMdxByHeadings(doc.content) 40 | 41 | for (const section of sections) { 42 | if (!section || !section.content) continue 43 | 44 | const headingId = `${doc._meta.path}#${slugify(section.title!)}` 45 | 46 | const metadata = { 47 | title: section.title ?? "", 48 | path: doc._meta.path, 49 | level: section.level ?? 2, 50 | type: "section", 51 | content: section.content, 52 | documentTitle: doc.title, 53 | } satisfies SearchMetadata 54 | 55 | await index.upsert({ 56 | id: headingId, 57 | data: `${section.title}\n\n${section.content}`, 58 | metadata, 59 | }) 60 | } 61 | 62 | console.log(`✅ Indexed document sections: ${doc.title}`) 63 | } catch (error) { 64 | console.error(`❌ Failed to index ${doc.title}:`, error) 65 | } 66 | } 67 | 68 | console.log("✅ Finished indexing docs") 69 | } 70 | 71 | indexDocs().catch(console.error) 72 | -------------------------------------------------------------------------------- /www/src/docs/backend/api-client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Client 3 | summary: The API Client in JStack 4 | --- 5 | 6 | # Type-Safe API Client 7 | 8 | One of the best features of JStack is its **end-to-end type safety** 😎. Let's see how to make type-safe API calls - so TypeScript knows exactly what data to expect from our server. 9 | 10 | --- 11 | 12 | ## 1. Client Setup 13 | 14 | Create a type-safe client that knows about all your API routes by passing it the `AppRouter` type - the type of your entire backend: 15 | 16 | ```ts lib/client.ts /AppRouter/ 17 | import { createClient } from "jstack" 18 | import type { AppRouter } from "@/server" 19 | 20 | export const client = createClient({ 21 | baseUrl: `${getBaseUrl()}/api`, 22 | }) 23 | 24 | function getBaseUrl() { 25 | // 👇 Adjust for wherever you deploy 26 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` 27 | return `http://localhost:3000` 28 | } 29 | ``` 30 | 31 | --- 32 | 33 | ## 2. Client Usage 34 | 35 | You can now make API calls from anywhere in your application with full type-safety 🎉. 36 | 37 | ```tsx app/page.tsx {3-4} 38 | import { client } from "@/lib/client" 39 | 40 | const res = await client.post.recent.$get() 41 | const post = await res.json() 42 | // ^ TypeScript knows this route's return type 43 | ``` 44 | 45 | --- 46 | 47 | ## 3. Example with React Query 48 | 49 | JStack's client works anywhere and **with any state manager** because it's simply a type-safe `fetch` wrapper. For example, it pairs perfectly with React Query: 50 | 51 | ```tsx app/page.tsx {4, 7-13} 52 | "use client" 53 | 54 | import { client } from "@/lib/client" 55 | import { useQuery } from "@tanstack/react-query" 56 | 57 | export default function Page() { 58 | const { data, isLoading } = useQuery({ 59 | queryKey: ["get-recent-post"], 60 | queryFn: async () => { 61 | const res = await client.post.recent.$get() 62 | return await res.json() 63 | }, 64 | }) 65 | 66 | if (isLoading) return

Loading...

67 | 68 | return

{data.title}

// TypeScript knows this is safe! 69 | } 70 | ``` 71 | 72 | You can use the client with any other state manager you prefer, such as Zustand, Jotai, or Redux. JStack does not care 🤷‍♂️. -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "DOM.Iterable", "ES2021"], 5 | "module": "Node16", 6 | "moduleResolution": "nodenext", 7 | "resolveJsonModule": true, 8 | "allowJs": true, 9 | "checkJs": true, 10 | "baseUrl": "./", 11 | 12 | /* EMIT RULES */ 13 | "outDir": "./dist", 14 | "noEmit": true, // TSUP takes care of emitting js for us, in a MUCH faster way 15 | "declaration": true, 16 | "declarationMap": true, 17 | "sourceMap": true, 18 | "removeComments": true, 19 | 20 | /* TYPE CHECKING RULES */ 21 | "strict": true, 22 | // "noImplicitAny": true, // Included in "Strict" 23 | // "noImplicitThis": true, // Included in "Strict" 24 | // "strictBindCallApply": true, // Included in "Strict" 25 | // "strictFunctionTypes": true, // Included in "Strict" 26 | // "strictNullChecks": true, // Included in "Strict" 27 | // "strictPropertyInitialization": true, // Included in "Strict" 28 | "noFallthroughCasesInSwitch": true, 29 | "noImplicitOverride": true, 30 | "noImplicitReturns": true, 31 | // "noUnusedLocals": true, 32 | // "noUnusedParameters": true, 33 | "useUnknownInCatchVariables": true, 34 | "noUncheckedIndexedAccess": true, // TLDR - Checking an indexed value (array[0]) now forces type as there is no confirmation that index exists 35 | // THE BELOW ARE EXTRA STRICT OPTIONS THAT SHOULD ONLY BY CONSIDERED IN VERY SAFE PROJECTS 36 | // "exactOptionalPropertyTypes": true, // TLDR - Setting to undefined is not the same as a property not being defined at all 37 | // "noPropertyAccessFromIndexSignature": true, // TLDR - Use dot notation for objects if youre sure it exists, use ['index'] notaion if unsure 38 | 39 | /* OTHER OPTIONS */ 40 | "allowSyntheticDefaultImports": true, 41 | "esModuleInterop": true, 42 | // "emitDecoratorMetadata": true, 43 | // "experimentalDecorators": true, 44 | "forceConsistentCasingInFileNames": true, 45 | "skipLibCheck": true, 46 | "useDefineForClassFields": true, 47 | 48 | "paths": { 49 | "@/*": ["./src/*"] 50 | } 51 | }, 52 | "include": ["src", "tsup.config.ts", "../reset.d.ts", "prettier.config.mjs"] 53 | } 54 | -------------------------------------------------------------------------------- /www/src/docs/getting-started/environment-variables.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Environment Variables 3 | summary: Managing environment variables for different deployment targets 4 | --- 5 | 6 | # Environment Variables in JStack 7 | 8 | JStack supports multiple deployment targets such as Cloudflare Workers, Vercel, Netlify, and more. This guide will help you set up environment variables correctly based on your deployment platform. 9 | 10 | JStack supports **full environment variable type-safety**: 11 | 12 | ```ts server/jstack.ts {3-5,7} 13 | import { jstack } from "jstack" 14 | 15 | interface Env { 16 | Bindings: { DATABASE_URL: string } 17 | } 18 | 19 | export const j = jstack.init() 20 | 21 | /** 22 | * Public (unauthenticated) procedures 23 | * This is the base part you use to create new procedures. 24 | */ 25 | export const publicProcedure = j.procedure 26 | ``` 27 | 28 | --- 29 | 30 | ## Node.js (Vercel, Netlify, etc.) 31 | 32 | If you deploy JStack in a Node.js environment (e.g., Vercel, Netlify), both the frontend and the backend will run on Node.js. You can access environment variables anywhere using `process.env`. 33 | 34 | 1. **Local development** 35 | 36 | ```plaintext .env 37 | DATABASE_URL=your-database-url 38 | ``` 39 | 40 | 2. **Accessing variables** 41 | ```ts 42 | // Works everywhere (frontend & backend) 43 | const DATABASE_URL = process.env.DATABASE_URL 44 | ``` 45 | 46 | --- 47 | 48 | ## Cloudflare Workers 49 | 50 | When using Cloudflare Workers (either locally with `wrangler dev` or in production), you'll need to use [Cloudflare's environment variable system](https://developers.cloudflare.com/workers/configuration/environment-variables/): 51 | 52 | 1. **Local development** 53 | 54 | ```plaintext .dev.vars 55 | DATABASE_URL=your-database-url 56 | ``` 57 | 58 | 2. **Accessing variables** 59 | 60 | ```ts 61 | // Frontend (client & server components) 62 | const DATABASE_URL = process.env.DATABASE_URL 63 | ``` 64 | 65 | ```ts {2, 7} 66 | // Backend (API) 67 | import { env } from "hono/adapter" 68 | import { j } from "jstack" 69 | 70 | export const postRouter = j.router({ 71 | recent: j.procedure.get(({ c }) => { 72 | const { DATABASE_URL } = env(c) 73 | }), 74 | }) 75 | ``` 76 | -------------------------------------------------------------------------------- /www/src/docs/backend/routers.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Routers 3 | summary: Routers in JStack 4 | --- 5 | 6 | # About Routers 7 | 8 | A router in JStack is a **collection of procedures** (API endpoints) related to a specific feature or resource. For example: 9 | 10 | - `userRouter` for user management operations 11 | - `postRouter` for blog post operations 12 | - `paymentRouter` for payment related endpoints 13 | 14 | ```plaintext {5-8} 15 | app/ 16 | └── server/ 17 | ├── jstack.ts # Initializing JStack 18 | ├── index.ts # Main appRouter 19 | └── routers/ # Router directory 20 | ├── user-router.ts 21 | ├── post-router.ts 22 | └── payment-router.ts 23 | ``` 24 | 25 | --- 26 | 27 | ## Creating a Router 28 | 29 | 1. Create a new file in `server/routers`: 30 | 31 | ```ts server/routers/post-router.ts 32 | import { j } from "../jstack" 33 | 34 | export const postRouter = j.router({ 35 | // Procedures go here... 36 | }) 37 | ``` 38 | 39 | 2. Add procedures to your router: 40 | 41 | ```ts {4-10} server/routers/post-router.ts 42 | import { j, publicProcedure } from "../jstack" 43 | 44 | export const postRouter = j.router({ 45 | list: publicProcedure.get(({ c }) => { 46 | return c.json({ posts: [] }) 47 | }), 48 | 49 | create: publicProcedure.post(({ c }) => { 50 | return c.json({ success: true }) 51 | }), 52 | }) 53 | ``` 54 | 55 | 3. Register your router with the main `appRouter`: 56 | 57 | ```ts server/index.ts {2, 11} 58 | import { j } from "./jstack" 59 | import { postRouter } from "./routers/post-router" 60 | 61 | const api = j 62 | .router() 63 | .basePath("/api") 64 | .use(j.defaults.cors) 65 | .onError(j.defaults.errorHandler) 66 | 67 | const appRouter = j.mergeRouters(api, { 68 | post: postRouter, 69 | }) 70 | 71 | export type AppType = typeof appRouter 72 | 73 | export default appRouter 74 | ``` 75 | 76 | Under the hood, each procedure is a separate HTTP endpoint. The URL structure is as follows: 77 | 78 | - The base path of your API (`/api`) 79 | - The router name (`post`) 80 | - The procedure name (`list`) 81 | 82 | For example, the `list` procedure is now available at 83 | 84 | ```plaintext 85 | http://localhost:3000/api/post/list 86 | ``` 87 | -------------------------------------------------------------------------------- /www/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { Metadata } from "next" 3 | import { twMerge } from "tailwind-merge" 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | export function remToPx(remValue: number) { 10 | let rootFontSize = 11 | typeof window === "undefined" ? 16 : parseFloat(window.getComputedStyle(document.documentElement).fontSize) 12 | 13 | return remValue * rootFontSize 14 | } 15 | 16 | export function slugify(text: string) { 17 | return text 18 | .toString() 19 | .toLowerCase() 20 | .normalize(`NFD`) 21 | .trim() 22 | .replace(/\./g, ``) 23 | .replace(/\s+/g, `-`) 24 | .replace(/[^\w-]+/g, ``) 25 | .replace(/--+/g, `-`) 26 | } 27 | 28 | export function constructMetadata({ 29 | title = "JStack - Full-Stack Next.js & TypeScript Toolkit", 30 | description = "Build fast, reliable Next.js apps with the most modern web technologies.", 31 | image = "/thumbnail.png", 32 | icons = "/favicon.ico", 33 | }: { 34 | title?: string 35 | description?: string 36 | image?: string 37 | icons?: string 38 | noIndex?: boolean 39 | } = {}): Metadata { 40 | return { 41 | title, 42 | description, 43 | openGraph: { 44 | title, 45 | description, 46 | images: [ 47 | { 48 | url: image, 49 | }, 50 | ], 51 | }, 52 | twitter: { 53 | card: "summary_large_image", 54 | title, 55 | description, 56 | images: [image], 57 | creator: "@joshtriedcoding", 58 | }, 59 | icons, 60 | metadataBase: new URL("https://jstack.app"), 61 | } 62 | } 63 | 64 | export const levenshtein = (a: string, b: string): number => { 65 | if (a.length === 0) return b.length 66 | if (b.length === 0) return a.length 67 | 68 | const matrix: number[][] = Array(b.length + 1) 69 | .fill(null) 70 | .map(() => Array(a.length + 1).fill(null)) 71 | 72 | for (let i = 0; i <= a.length; i++) matrix[0]![i] = i 73 | for (let j = 0; j <= b.length; j++) matrix[j]![0] = j 74 | 75 | for (let j = 1; j <= b.length; j++) { 76 | for (let i = 1; i <= a.length; i++) { 77 | const cost = a[i - 1] === b[j - 1] ? 0 : 1 78 | matrix[j]![i] = Math.min( 79 | matrix[j]![i - 1]! + 1, 80 | matrix[j - 1]![i]! + 1, 81 | matrix[j - 1]![i - 1]! + cost, 82 | ) 83 | } 84 | } 85 | 86 | return matrix[b.length]![a.length]! 87 | } 88 | -------------------------------------------------------------------------------- /cli/src/installers/index.ts: -------------------------------------------------------------------------------- 1 | import { PackageManager } from "@/utils/get-user-pkg-manager.js" 2 | import { drizzleInstaller } from "./drizzle.js" 3 | import { neonInstaller } from "./neon.js" 4 | import { noOrmInstaller } from "./no-orm.js" 5 | import { postgresInstaller } from "./postgres.js" 6 | import { vercelPostgresInstaller } from "./vercel-postgres.js" 7 | import { planetscaleInstaller } from "./planetscale.js" 8 | 9 | // Turning this into a const allows the list to be iterated over for programmatically creating prompt options 10 | // Should increase extensibility in the future 11 | export const orms = ["none", "drizzle"] as const 12 | export type Orm = (typeof orms)[number] 13 | 14 | export const dialects = ["postgres"] as const 15 | export type Dialect = (typeof dialects)[number] 16 | 17 | export const providers = ["postgres", "neon", "vercel-postgres", "planetscale"] as const 18 | export type Provider = (typeof providers)[number] 19 | 20 | export type InstallerMap = { 21 | orm: { 22 | [key in Orm]: { 23 | inUse: boolean 24 | installer: Installer 25 | } 26 | } 27 | provider: { 28 | [key in Provider]: { 29 | inUse: boolean 30 | installer: Installer 31 | } 32 | } 33 | } 34 | 35 | export interface InstallerOptions { 36 | projectDir: string 37 | pkgManager: PackageManager 38 | noInstall: boolean 39 | installers: InstallerMap 40 | appRouter?: boolean 41 | projectName: string 42 | databaseProvider: Provider 43 | } 44 | 45 | export type Installer = (opts: InstallerOptions) => void 46 | 47 | export const buildInstallerMap = ( 48 | selectedOrm: Orm = "none", 49 | selectedProvider?: Provider 50 | ): InstallerMap => ({ 51 | orm: { 52 | none: { 53 | inUse: selectedOrm === "none", 54 | installer: noOrmInstaller, 55 | }, 56 | drizzle: { 57 | inUse: selectedOrm === "drizzle", 58 | installer: drizzleInstaller, 59 | }, 60 | }, 61 | provider: { 62 | postgres: { 63 | inUse: selectedProvider === "postgres", 64 | installer: postgresInstaller, 65 | }, 66 | neon: { 67 | inUse: selectedProvider === "neon", 68 | installer: neonInstaller, 69 | }, 70 | "vercel-postgres": { 71 | inUse: selectedProvider === "vercel-postgres", 72 | installer: vercelPostgresInstaller, 73 | }, 74 | planetscale: { 75 | inUse: selectedProvider === "planetscale", 76 | installer: planetscaleInstaller 77 | } 78 | }, 79 | }) 80 | -------------------------------------------------------------------------------- /www/src/server/routers/stargazers-router.ts: -------------------------------------------------------------------------------- 1 | import { fetchStargazers } from "@/actions/stargazers" 2 | import { j, publicProcedure } from "@/server/jstack" 3 | import { Receiver } from "@upstash/qstash" 4 | import { env } from "hono/adapter" 5 | 6 | type StargazerInfo = Awaited> 7 | 8 | export const stargazersRouter = j.router({ 9 | /** 10 | * Background task to maintain real-time GitHub stargazer data in Redis 11 | * 12 | * This endpoint is automatically called by QStash every 10s to: 13 | * 1. Fetch the latest stargazer data from GitHub 14 | * 2. Store it in Redis for fast retrieval 15 | * 16 | * @internal This endpoint is meant to be called by QStash scheduler only 17 | */ 18 | prefetch: publicProcedure.post(async ({ c, ctx }) => { 19 | const { redis } = ctx 20 | const { 21 | AMPLIFY_URL, 22 | QSTASH_CURRENT_SIGNING_KEY, 23 | QSTASH_NEXT_SIGNING_KEY, 24 | GITHUB_TOKEN, 25 | } = env(c) 26 | 27 | const ROUTER_URL = AMPLIFY_URL + "/api/stargazers/prefetch" 28 | 29 | const receiver = new Receiver({ 30 | currentSigningKey: QSTASH_CURRENT_SIGNING_KEY, 31 | nextSigningKey: QSTASH_NEXT_SIGNING_KEY, 32 | }) 33 | 34 | const signature = 35 | c.req.header("Upstash-Signature") || 36 | c.req.header("upstash-signature") || 37 | "" 38 | const body = await c.req.raw.text().catch(() => "{}") 39 | 40 | try { 41 | await receiver.verify({ 42 | body, 43 | signature, 44 | url: ROUTER_URL, 45 | }) 46 | } catch (err) { 47 | return c.json( 48 | { success: false, message: "Invalid request signature" }, 49 | 401, 50 | ) 51 | } 52 | 53 | const { stargazers, stargazerCount } = await fetchStargazers({ 54 | GITHUB_TOKEN, 55 | }) 56 | 57 | await redis.set("stargazer-info", { stargazerCount, stargazers }) 58 | 59 | return c.json({ success: true }) 60 | }), 61 | 62 | /** 63 | * This procedure fetches the most recent GitHub stargazers data from cache 64 | * 65 | * The data is served from Redis for performance, 66 | * updated every 5s by our background prefetch process. 67 | * 68 | * @returns {Promise} List of recent stargazers with metadata 69 | */ 70 | recent: publicProcedure.get(async ({ c, ctx }) => { 71 | const { redis } = ctx 72 | 73 | const stargazerInfo = await redis.get("stargazer-info") 74 | 75 | return c.json(stargazerInfo) 76 | }), 77 | }) 78 | -------------------------------------------------------------------------------- /www/src/app/docs/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { cn } from "@/lib/utils" 4 | import Link from "next/link" 5 | import { useEffect, useState } from "react" 6 | import { Icons } from "../../components/icons" 7 | import { Menu, X } from "lucide-react" 8 | import { DocNavigation } from "./doc-navigation" 9 | 10 | export function MobileNavigation() { 11 | const [isOpen, setIsOpen] = useState(false) 12 | 13 | useEffect(() => { 14 | if (isOpen) { 15 | document.body.style.overflow = "hidden" 16 | } else { 17 | document.body.style.overflow = "auto" 18 | } 19 | 20 | return () => { 21 | document.body.style.overflow = "auto" 22 | } 23 | }, [isOpen]) 24 | 25 | return ( 26 | <> 27 | 34 | {isOpen && ( 35 |
36 | )} 37 |
43 |
44 | setIsOpen(false)} 46 | href="/" 47 | aria-label="Home" 48 | className="flex h-full" 49 | > 50 |
51 | 52 |
53 |

54 | JStack 55 |

56 |

docs

57 |
58 |
59 | 60 | 68 |
69 | 70 |
71 | 72 | setIsOpen(false)} className="mt-2" /> 73 |
74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "pages:build": "bunx @cloudflare/next-on-pages", 9 | "deploy": "bun pages:build && wrangler pages deploy", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "env:sync": "tsx src/deploy.ts env:sync" 13 | }, 14 | "engines": { 15 | "node": ">=20.0.0" 16 | }, 17 | "dependencies": { 18 | "@content-collections/core": "^0.8.0", 19 | "@content-collections/mdx": "^0.2.0", 20 | "@content-collections/next": "^0.2.4", 21 | "@mdx-js/loader": "^3.1.0", 22 | "@mdx-js/react": "^3.1.0", 23 | "@next/mdx": "^15.1.6", 24 | "@radix-ui/react-avatar": "^1.1.2", 25 | "@radix-ui/react-dialog": "^1.1.4", 26 | "@radix-ui/react-scroll-area": "^1.2.2", 27 | "@radix-ui/react-tooltip": "^1.1.6", 28 | "@tanstack/react-query": "^5.51.23", 29 | "@uidotdev/usehooks": "^2.4.1", 30 | "@upstash/qstash": "^2.7.20", 31 | "@upstash/redis": "^1.34.3", 32 | "@upstash/vector": "^1.2.0", 33 | "class-variance-authority": "^0.7.1", 34 | "clsx": "^2.1.1", 35 | "drizzle-orm": "^0.36.1", 36 | "hono": "4.6.17", 37 | "jstack": "^1.0.3-alpha.2", 38 | "lodash.throttle": "^4.1.1", 39 | "lucide-react": "^0.469.0", 40 | "next": "^15.0.3", 41 | "postgres": "^3.4.5", 42 | "react": "^18.3.1", 43 | "react-dom": "^18.3.1", 44 | "rehype-autolink-headings": "^7.1.0", 45 | "rehype-mdx-code-props": "^3.0.1", 46 | "rehype-pretty-code": "^0.14.0", 47 | "rehype-slug": "^6.0.0", 48 | "rehype-stringify": "^10.0.1", 49 | "remark-parse": "^11.0.0", 50 | "remark-rehype": "^11.1.1", 51 | "shiki": "^1.26.0", 52 | "superjson": "^2.2.1", 53 | "tailwind-merge": "^2.6.0", 54 | "tailwind-scrollbar": "^3.1.0", 55 | "tailwindcss-animate": "^1.0.7", 56 | "wrangler": "^3.72.0", 57 | "zod": "^3.23.8", 58 | "zustand": "^5.0.2" 59 | }, 60 | "devDependencies": { 61 | "@cloudflare/next-on-pages": "^1.13.7", 62 | "@cloudflare/workers-types": "^4.20240815.0", 63 | "@shikijs/transformers": "^1.26.1", 64 | "@types/lodash.throttle": "^4.1.9", 65 | "@types/node": "^20", 66 | "@types/react": "^18", 67 | "@types/react-dom": "^18", 68 | "boxen": "^8.0.1", 69 | "dotenv": "^16.4.5", 70 | "drizzle-kit": "^0.28.0", 71 | "eslint": "^8", 72 | "eslint-config-next": "14.2.5", 73 | "ora": "^8.1.1", 74 | "postcss": "^8", 75 | "tailwindcss": "^3.4.1", 76 | "typescript": "^5" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /www/src/docs/deploy/vercel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vercel 3 | summary: Deploy JStack to Vercel 4 | --- 5 | 6 | # Deploy to Vercel 7 | 8 | **Deploying JStack to Vercel is like deploying any other Next.js app**, it works out of the box. This guide will walk you through the deployment process. 9 | 10 | --- 11 | 12 | ## Deployment Steps 13 | 14 | 1. **Configure Client URL** 15 | 16 | Update `lib/client.ts` to use the Vercel URL in production. Vercel automatically provides your deployment URL as `process.env.VERCEL_URL`: 17 | 18 | ```ts lib/client.ts {5,9-17} 19 | import type { AppRouter } from "@/server" 20 | import { createClient } from "jstack" 21 | 22 | export const client = createClient({ 23 | baseUrl: `${getBaseUrl()}/api`, 24 | }) 25 | 26 | function getBaseUrl() { 27 | // 👇 Use browser URL if client-side 28 | if (typeof window !== "undefined") { 29 | return window.location.origin 30 | } 31 | 32 | // 👇 Use Vercel URL in production 33 | if (process.env.VERCEL_URL) { 34 | return `https://${process.env.VERCEL_URL}` 35 | } 36 | 37 | // 👇 Default to localhost 38 | return `http://localhost:3000` 39 | } 40 | ``` 41 | 42 | 2. **Deploy to Vercel** 43 | 44 | - Via GitHub: Connect repository through Vercel dashboard 45 | - Via CLI: Run `vercel deploy` 46 | - Vercel automatically sets the production URL 47 | 48 | --- 49 | 50 | ## Environment Variables 51 | 52 | Configure your environment variables in the Vercel dashboard: 53 | 54 | - Go to your project settings 55 | - Navigate to the "Environment Variables" tab 56 | - Add your variables 57 | 58 | 59 | Environment variables for Vercel deployment 63 | 64 | 65 | Alternatively, you can use the CLI to add environment variables: 66 | 67 | ```bash Terminal 68 | vercel env add 69 | ``` 70 | 71 | --- 72 | 73 | ## Common Problems 74 | 75 | ### CORS Configuration 76 | 77 | If you're experiencing CORS problems, make sure your base API includes CORS middleware: 78 | 79 | ```ts server/index.ts {8} 80 | import { InferRouterInputs, InferRouterOutputs } from "jstack" 81 | import { postRouter } from "./routers/post-router" 82 | import { j } from "./jstack" 83 | 84 | const api = j 85 | .router() 86 | .basePath("/api") 87 | .use(j.defaults.cors) 88 | .onError(j.defaults.errorHandler) 89 | 90 | const appRouter = j.mergeRouters(api, { 91 | post: postRouter, 92 | }) 93 | 94 | export type AppRouter = typeof appRouter 95 | 96 | export default appRouter 97 | ``` 98 | 99 | [→ More about CORS in JStack](/docs/backend/app-router#cors) 100 | -------------------------------------------------------------------------------- /cli/src/helpers/install-deps.ts: -------------------------------------------------------------------------------- 1 | import { getUserPkgManager, PackageManager } from "@/utils/get-user-pkg-manager.js" 2 | import { logger } from "@/utils/logger.js" 3 | import chalk from "chalk" 4 | import { execa, type Options } from "execa" 5 | import ora, { type Ora } from "ora" 6 | 7 | const execWithSpinner = async ( 8 | projectDir: string, 9 | pkgManager: PackageManager, 10 | options: { 11 | args?: string[] 12 | stdout?: Options["stdout"] 13 | onDataHandle?: (spinner: Ora) => (data: Buffer) => void 14 | } 15 | ) => { 16 | const { onDataHandle, args = ["install"], stdout = "pipe" } = options 17 | 18 | const spinner = ora(`Running ${pkgManager} install...`).start() 19 | const subprocess = execa(pkgManager, args, { cwd: projectDir, stdout }) 20 | 21 | await new Promise((res, rej) => { 22 | if (onDataHandle) { 23 | subprocess.stdout?.on("data", onDataHandle(spinner)) 24 | } 25 | 26 | void subprocess.on("error", (e) => rej(e)) 27 | void subprocess.on("close", () => res()) 28 | }) 29 | 30 | return spinner 31 | } 32 | 33 | const runInstallCommand = async (pkgManager: PackageManager, projectDir: string): Promise => { 34 | switch (pkgManager) { 35 | // When using npm, inherit the stderr stream so that the progress bar is shown 36 | case "npm": 37 | await execa(pkgManager, ["install"], { 38 | cwd: projectDir, 39 | stderr: "inherit", 40 | }) 41 | 42 | return null 43 | 44 | // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress 45 | case "pnpm": 46 | return execWithSpinner(projectDir, pkgManager, { 47 | onDataHandle: (spinner) => (data) => { 48 | const text = data.toString() 49 | 50 | if (text.includes("Progress")) { 51 | spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text 52 | } 53 | }, 54 | }) 55 | 56 | case "yarn": 57 | return execWithSpinner(projectDir, pkgManager, { 58 | onDataHandle: (spinner) => (data) => { 59 | spinner.text = data.toString() 60 | }, 61 | }) 62 | 63 | // When using bun, the stdout stream is ignored and the spinner is shown 64 | case "bun": 65 | return execWithSpinner(projectDir, pkgManager, { stdout: "ignore" }) 66 | } 67 | } 68 | 69 | export const installDependencies = async ({ projectDir }: { projectDir: string }) => { 70 | logger.info("Installing dependencies...") 71 | const pkgManager = getUserPkgManager() 72 | 73 | const installSpinner = await runInstallCommand(pkgManager, projectDir) 74 | 75 | // If the spinner was used to show the progress, use succeed method on it 76 | // If not, use the succeed on a new spinner 77 | ;(installSpinner ?? ora()).succeed(chalk.green("Successfully installed dependencies!\n")) 78 | } 79 | -------------------------------------------------------------------------------- /app/src/app/components/post.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" 4 | import { useState } from "react" 5 | import { client } from "@/lib/client" 6 | 7 | export const RecentPost = () => { 8 | const [name, setName] = useState("") 9 | const queryClient = useQueryClient() 10 | 11 | const { data: recentPost, isPending: isLoadingPosts } = useQuery({ 12 | queryKey: ["get-recent-post"], 13 | queryFn: async () => { 14 | const res = await client.post.recent.$get() 15 | return await res.json() 16 | }, 17 | }) 18 | 19 | const { mutate: createPost, isPending } = useMutation({ 20 | mutationFn: async ({ name }: { name: string }) => { 21 | const res = await client.post.create.$post({ name }) 22 | return await res.json() 23 | }, 24 | onSuccess: async () => { 25 | await queryClient.invalidateQueries({ queryKey: ["get-recent-post"] }) 26 | setName("") 27 | }, 28 | }) 29 | 30 | return ( 31 |
32 | {isLoadingPosts ? ( 33 |

34 | Loading posts... 35 |

36 | ) : recentPost ? ( 37 |

38 | Your recent post: "{recentPost.name}" 39 |

40 | ) : ( 41 |

42 | You have no posts yet. 43 |

44 | )} 45 |
{ 47 | e.preventDefault() 48 | createPost({ name }) 49 | }} 50 | onKeyDown={(e) => { 51 | if (e.key === "Enter" && !e.shiftKey) { 52 | e.preventDefault() 53 | createPost({ name }) 54 | } 55 | }} 56 | className="flex flex-col gap-4" 57 | > 58 | setName(e.target.value)} 63 | className="w-full text-base/6 rounded-md bg-black/50 hover:bg-black/75 focus-visible:outline-none ring-2 ring-transparent hover:ring-zinc-800 focus:ring-zinc-800 focus:bg-black/75 transition h-12 px-4 py-2 text-zinc-100" 64 | /> 65 | 72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /cli/template/base/src/app/components/post.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" 4 | import { useState } from "react" 5 | import { client } from "@/lib/client" 6 | 7 | export const RecentPost = () => { 8 | const [name, setName] = useState("") 9 | const queryClient = useQueryClient() 10 | 11 | const { data: recentPost, isPending: isLoadingPosts } = useQuery({ 12 | queryKey: ["get-recent-post"], 13 | queryFn: async () => { 14 | const res = await client.post.recent.$get() 15 | return await res.json() 16 | }, 17 | }) 18 | 19 | const { mutate: createPost, isPending } = useMutation({ 20 | mutationFn: async ({ name }: { name: string }) => { 21 | const res = await client.post.create.$post({ name }) 22 | return await res.json() 23 | }, 24 | onSuccess: async () => { 25 | await queryClient.invalidateQueries({ queryKey: ["get-recent-post"] }) 26 | setName("") 27 | }, 28 | }) 29 | 30 | return ( 31 |
32 | {isLoadingPosts ? ( 33 |

34 | Loading posts... 35 |

36 | ) : recentPost ? ( 37 |

38 | Your recent post: "{recentPost.name}" 39 |

40 | ) : ( 41 |

42 | You have no posts yet. 43 |

44 | )} 45 |
{ 47 | e.preventDefault() 48 | createPost({ name }) 49 | }} 50 | onKeyDown={(e) => { 51 | if (e.key === "Enter" && !e.shiftKey) { 52 | e.preventDefault() 53 | createPost({ name }) 54 | } 55 | }} 56 | className="flex flex-col gap-4" 57 | > 58 | setName(e.target.value)} 63 | className="w-full text-base/6 rounded-md bg-black/50 hover:bg-black/75 focus-visible:outline-none ring-2 ring-transparent hover:ring-zinc-800 focus:ring-zinc-800 focus:bg-black/75 transition h-12 px-4 py-2 text-zinc-100" 64 | /> 65 | 72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /www/src/components/table-of-contents.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTableOfContents } from "@/ctx/use-table-of-contents" 4 | import { cn, slugify } from "@/lib/utils" 5 | import { AlignLeft } from "lucide-react" 6 | import Link from "next/link" 7 | import { HTMLAttributes, useCallback, useEffect } from "react" 8 | 9 | interface TableOfContentsProps extends HTMLAttributes {} 10 | 11 | export const TableOfContents = ({ 12 | className, 13 | ...props 14 | }: TableOfContentsProps) => { 15 | const visibleSections = useTableOfContents((state) => state.visibleSections) 16 | const allHeadings = useTableOfContents((state) => state.allHeadings) 17 | const setVisibleSections = useTableOfContents( 18 | (state) => state.setVisibleSections, 19 | ) 20 | 21 | useEffect(() => { 22 | if (!allHeadings[0]) return 23 | 24 | if (allHeadings.length > 0 && visibleSections.length === 0) { 25 | const firstHeadingSlug = slugify(allHeadings[0].text) 26 | setVisibleSections([firstHeadingSlug]) 27 | } 28 | }, [allHeadings, visibleSections, setVisibleSections]) 29 | 30 | const handleClick = useCallback( 31 | (headingText: string) => { 32 | const slug = slugify(headingText) 33 | setVisibleSections([slug]) 34 | }, 35 | [setVisibleSections], 36 | ) 37 | 38 | return ( 39 |
40 |
41 |

42 | On this page 43 |

44 |
    45 | {allHeadings.map((heading, i) => { 46 | const isVisible = visibleSections.some( 47 | (section) => section === slugify(heading.text), 48 | ) 49 | 50 | return ( 51 |
  • 52 | handleClick(heading.text)} 55 | className="relative flex" 56 | > 57 |
    63 |

    72 | {heading.text} 73 |

    74 | 75 |
  • 76 | ) 77 | })} 78 |
79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /cli/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { intro, isCancel, outro, select, text } from "@clack/prompts" 2 | import color from "picocolors" 3 | import { getUserPkgManager } from "@/utils/get-user-pkg-manager.js" 4 | 5 | export interface CliResults { 6 | projectName: string 7 | orm: "none" | "drizzle" | undefined 8 | dialect?: "postgres" | undefined 9 | provider?: "neon" | "postgres" | "vercel-postgres" | "planetscale" | undefined 10 | noInstall?: boolean 11 | } 12 | 13 | export type Dialect = CliResults["dialect"] 14 | export type Orm = CliResults["orm"] 15 | 16 | export async function runCli(): Promise { 17 | console.clear() 18 | 19 | // Parse command line arguments manually 20 | const args = process.argv.slice(2) 21 | const cliProvidedName = args[0]?.startsWith("--") ? undefined : args[0] 22 | const noInstallFlag = args.includes("--noInstall") 23 | 24 | intro(color.bgCyan(" jStack CLI ")) 25 | 26 | const projectName = 27 | cliProvidedName || 28 | (await text({ 29 | message: "What will your project be called?", 30 | placeholder: "my-jstack-app", 31 | validate: (value) => { 32 | if (!value) return "Please enter a project name" 33 | if (value.length > 50) return "Project name must be less than 50 characters" 34 | return 35 | }, 36 | })) 37 | 38 | if (isCancel(projectName)) { 39 | outro("Setup cancelled.") 40 | return undefined 41 | } 42 | 43 | const orm = await select<"none" | "drizzle">({ 44 | message: "Which database ORM would you like to use?", 45 | options: [ 46 | { value: "none", label: "None" }, 47 | { value: "drizzle", label: "Drizzle ORM" }, 48 | ], 49 | }) 50 | 51 | if (isCancel(orm)) { 52 | outro("Setup cancelled.") 53 | return undefined 54 | } 55 | 56 | let dialect = undefined 57 | let provider = undefined 58 | if (orm === "drizzle") { 59 | dialect = "postgres" as const // Only offering postgres 60 | 61 | provider = await select({ 62 | message: "Which Postgres provider would you like to use?", 63 | options: [ 64 | { value: "postgres", label: "PostgreSQL" }, 65 | { value: "neon", label: "Neon" }, 66 | { value: "vercel-postgres", label: "Vercel Postgres" }, 67 | ], 68 | }) 69 | 70 | if (isCancel(provider)) { 71 | outro("Setup cancelled.") 72 | return undefined 73 | } 74 | } 75 | 76 | let noInstall = noInstallFlag 77 | if (!noInstall) { 78 | const pkgManager = getUserPkgManager() 79 | const shouldInstall = await select({ 80 | message: `Should we run '${pkgManager}${pkgManager === "yarn" ? "" : " install"}' for you?`, 81 | options: [ 82 | { value: false, label: "Yes" }, 83 | { value: true, label: "No" }, 84 | ], 85 | }) 86 | 87 | if (isCancel(shouldInstall)) { 88 | outro("Setup cancelled.") 89 | return undefined 90 | } 91 | 92 | noInstall = shouldInstall 93 | } 94 | 95 | return { 96 | projectName: projectName as string, 97 | orm, 98 | dialect, 99 | provider, 100 | noInstall, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /www/src/docs/deploy/cloudflare.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cloudflare Workers 3 | summary: Deploy JStack to Cloudflare Workers 4 | --- 5 | 6 | # Deploy to Cloudflare Workers 7 | 8 | JStack can be deployed to Cloudflare Workers, providing a globally distributed, serverless runtime for your API. This guide will walk you through the deployment process. 9 | 10 | --- 11 | 12 | ## Prerequisites 13 | 14 | 1. Install the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) 15 | 16 | ```bash Terminal 17 | npm install wrangler@latest -g 18 | ``` 19 | 20 | 2. Make sure you have an account at [Cloudflare](https://www.cloudflare.com/) 21 | 22 | --- 23 | 24 | ## Deployment Steps 25 | 26 | 1. Deploy your backend to Cloudflare Workers using `wrangler deploy`. Enter the path to your `appRouter` file, by default this is: 27 | 28 | ```bash Terminal 29 | wrangler deploy src/server/index.ts 30 | ``` 31 | 32 | The console output will look like this: 33 | 34 | 35 | Deploy JStack to Cloudflare Workers 36 | 37 | 38 | 2. Add the deployment URL to the client: 39 | 40 | ```ts lib/client.ts {5,8-16} 41 | import type { AppRouter } from "@/server" 42 | import { createClient } from "jstack" 43 | 44 | export const client = createClient({ 45 | baseUrl: `${getBaseUrl()}/api`, 46 | }) 47 | 48 | function getBaseUrl() { 49 | // 👇 In production, use the production worker 50 | if (process.env.NODE_ENV === "production") { 51 | return "https://.workers.dev" 52 | } 53 | 54 | // 👇 Locally, use wrangler backend 55 | return `http://localhost:8080` 56 | } 57 | ``` 58 | 59 | --- 60 | 61 | ## Environment Variables 62 | 63 | Make sure your Worker has the necessary environment variables configured. Either enter [one at a time](https://developers.cloudflare.com/workers/configuration/secrets/) or [update them in bulk](https://developers.cloudflare.com/workers/wrangler/commands/#secretbulk): 64 | 65 | ```bash Terminal 66 | wrangler secret put 67 | ``` 68 | 69 | --- 70 | 71 | ## Production Deployment 72 | 73 | When you deploy your front-end application: 74 | 75 | - Deploy to your preferred hosting platform (Vercel, Netlify, etc.) 76 | - After adding the deployment URL to your `lib/client.ts` file, your frontend will automatically connect to your Worker 77 | 78 | --- 79 | 80 | ## Common Problems 81 | 82 | ### CORS Configuration 83 | 84 | If you are experiencing CORS problems, make sure your Worker is configured correctly: 85 | 86 | ```ts server/index.ts {8} 87 | import { InferRouterInputs, InferRouterOutputs } from "jstack" 88 | import { postRouter } from "./routers/post-router" 89 | import { j } from "./jstack" 90 | 91 | const api = j 92 | .router() 93 | .basePath("/api") 94 | .use(j.defaults.cors) 95 | .onError(j.defaults.errorHandler) 96 | 97 | const appRouter = j.mergeRouters(api, { 98 | post: postRouter, 99 | }) 100 | 101 | export type AppRouter = typeof appRouter 102 | 103 | export default appRouter 104 | ``` 105 | 106 | [→ More about CORS in JStack](/docs/backend/app-router#cors) 107 | -------------------------------------------------------------------------------- /www/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | import { fontFamily } from "tailwindcss/defaultTheme" 3 | 4 | const config: Config = { 5 | darkMode: ["class"], 6 | content: [ 7 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | extend: { 13 | maxWidth: { 14 | "8xl": "88rem", 15 | }, 16 | fontFamily: { 17 | heading: ["var(--font-heading)", ...fontFamily.sans], 18 | noto: ["var(--font-noto)", ...fontFamily.sans], 19 | code: ["var(--font-code)", ...fontFamily.sans], 20 | }, 21 | colors: { 22 | gray: { 23 | "25": "#FCFCFD", 24 | "50": "#F9FAFB", 25 | "100": "#F2F4F7", 26 | "200": "#E4E7EC", 27 | "300": "#D0D5DD", 28 | "400": "#98A2B3", 29 | "500": "#667085", 30 | "600": "#475467", 31 | "700": "#344054", 32 | "800": "#182230", 33 | "900": "#101828", 34 | "950": "#0C111D", 35 | }, 36 | brand: { 37 | "25": "#FFF9F0", 38 | "50": "#FFF4E3", 39 | "100": "#FFE8C7", 40 | "200": "#FFD89B", 41 | "300": "#FFC66E", 42 | "400": "#FFB441", 43 | "500": "#F5A014", 44 | "600": "#D68508", 45 | "700": "#B36C06", 46 | "800": "#8F5404", 47 | "900": "#704103", 48 | "950": "#523002", 49 | }, 50 | ["dark-gray"]: "#2e2e32", 51 | background: "hsl(var(--background))", 52 | foreground: "hsl(var(--foreground))", 53 | card: { 54 | DEFAULT: "hsl(var(--card))", 55 | foreground: "hsl(var(--card-foreground))", 56 | }, 57 | popover: { 58 | DEFAULT: "hsl(var(--popover))", 59 | foreground: "hsl(var(--popover-foreground))", 60 | }, 61 | primary: { 62 | DEFAULT: "hsl(var(--primary))", 63 | foreground: "hsl(var(--primary-foreground))", 64 | }, 65 | secondary: { 66 | DEFAULT: "hsl(var(--secondary))", 67 | foreground: "hsl(var(--secondary-foreground))", 68 | }, 69 | muted: { 70 | light: "hsl(var(--muted-light))", 71 | dark: "hsl(var(--muted-dark))", 72 | foreground: "hsl(var(--muted-foreground))", 73 | DEFAULT: "hsl(var(--muted))", 74 | }, 75 | accent: { 76 | DEFAULT: "hsl(var(--accent))", 77 | foreground: "hsl(var(--accent-foreground))", 78 | }, 79 | destructive: { 80 | DEFAULT: "hsl(var(--destructive))", 81 | foreground: "hsl(var(--destructive-foreground))", 82 | }, 83 | border: "hsl(var(--border))", 84 | input: "hsl(var(--input))", 85 | ring: "hsl(var(--ring))", 86 | chart: { 87 | "1": "hsl(var(--chart-1))", 88 | "2": "hsl(var(--chart-2))", 89 | "3": "hsl(var(--chart-3))", 90 | "4": "hsl(var(--chart-4))", 91 | "5": "hsl(var(--chart-5))", 92 | }, 93 | }, 94 | borderRadius: { 95 | lg: "var(--radius)", 96 | md: "calc(var(--radius) - 2px)", 97 | sm: "calc(var(--radius) - 4px)", 98 | }, 99 | }, 100 | }, 101 | plugins: [require("tailwindcss-animate"), require("tailwind-scrollbar")], 102 | } 103 | export default config 104 | -------------------------------------------------------------------------------- /cli/src/helpers/install-base-template.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import * as p from "@clack/prompts" 3 | import chalk from "chalk" 4 | import fs from "fs-extra" 5 | import ora from "ora" 6 | 7 | import { PKG_ROOT } from "@/constants.js" 8 | import { type InstallerOptions } from "@/installers/index.js" 9 | import { logger } from "@/utils/logger.js" 10 | 11 | // This bootstraps the base Next.js application 12 | export const installBaseTemplate = async ({ 13 | projectName, 14 | projectDir, 15 | pkgManager, 16 | noInstall, 17 | }: InstallerOptions) => { 18 | const srcDir = path.join(PKG_ROOT, "template/base") 19 | const baseAssetsDir = path.join(PKG_ROOT, "template/base-assets") 20 | 21 | if (!noInstall) { 22 | logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`) 23 | } else { 24 | logger.info("") 25 | } 26 | 27 | const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start() 28 | 29 | if (fs.existsSync(projectDir)) { 30 | if (fs.readdirSync(projectDir).length === 0) { 31 | if (projectName !== ".") 32 | spinner.info( 33 | `${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`, 34 | ) 35 | } else { 36 | spinner.stopAndPersist() 37 | const overwriteDir = await p.select({ 38 | message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold( 39 | projectName, 40 | )} already exists and isn't empty. How would you like to proceed?`, 41 | options: [ 42 | { 43 | label: "Abort installation (recommended)", 44 | value: "abort", 45 | }, 46 | { 47 | label: "Clear the directory and continue installation", 48 | value: "clear", 49 | }, 50 | { 51 | label: "Continue installation and overwrite conflicting files", 52 | value: "overwrite", 53 | }, 54 | ], 55 | initialValue: "abort", 56 | }) 57 | if (overwriteDir === "abort") { 58 | spinner.fail("Aborting installation...") 59 | process.exit(1) 60 | } 61 | 62 | const overwriteAction = 63 | overwriteDir === "clear" 64 | ? "clear the directory" 65 | : "overwrite conflicting files" 66 | 67 | const confirmOverwriteDir = await p.confirm({ 68 | message: `Are you sure you want to ${overwriteAction}?`, 69 | initialValue: false, 70 | }) 71 | 72 | if (!confirmOverwriteDir) { 73 | spinner.fail("Aborting installation...") 74 | process.exit(1) 75 | } 76 | 77 | if (overwriteDir === "clear") { 78 | spinner.info( 79 | `Emptying ${chalk.cyan.bold(projectName)} and creating JStack app..\n`, 80 | ) 81 | fs.emptyDirSync(projectDir) 82 | } 83 | } 84 | } 85 | 86 | spinner.start() 87 | 88 | fs.copySync(srcDir, projectDir) 89 | 90 | // use package.json from base-assets instead of template/base 91 | fs.removeSync(path.join(projectDir, "package.json")) 92 | const packageJsonPath = path.join(baseAssetsDir, "base-package.json") 93 | fs.copySync(packageJsonPath, path.join(projectDir, "package.json")) 94 | 95 | fs.renameSync( 96 | path.join(projectDir, "_gitignore"), 97 | path.join(projectDir, ".gitignore"), 98 | ) 99 | 100 | const scaffoldedName = 101 | projectName === "." ? "App" : chalk.cyan.bold(projectName) 102 | 103 | spinner.succeed( 104 | `${scaffoldedName} ${chalk.green("scaffolded successfully!")}\n`, 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /www/src/actions/stargazers.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | interface Stargazer { 4 | id: number 5 | login: string 6 | avatar_url: string 7 | } 8 | 9 | type GithubResponse = { 10 | stargazers: Stargazer[] 11 | stargazerCount: number 12 | } 13 | 14 | export const fetchStargazers = async ({ GITHUB_TOKEN }: { GITHUB_TOKEN: string }): Promise => { 15 | if(!GITHUB_TOKEN) { 16 | throw new Error("GitHub token is required but was not provided. Set the GITHUB_TOKEN environment variable.") 17 | } 18 | 19 | const makeRequest = async (url: string, useToken = true) => { 20 | const headers: Record = { 21 | Accept: "application/vnd.github+json", 22 | "X-GitHub-Api-Version": "2022-11-28", 23 | "User-Agent": "Cloudflare Workers", 24 | } 25 | 26 | if (useToken) { 27 | headers.Authorization = `Bearer ${GITHUB_TOKEN}` 28 | } 29 | 30 | const res = await fetch(url, { headers }) 31 | 32 | if (res.status === 403) { 33 | // rate limit reached, retry without token 34 | if (useToken) { 35 | return makeRequest(url, false) 36 | } 37 | 38 | throw new Error("Rate limit reached for both authenticated and unauthenticated requests") 39 | } 40 | 41 | if (!res.ok) { 42 | const errorText = await res.text() 43 | throw new Error(`GitHub API failed: ${res.status} ${res.statusText} - ${errorText}`) 44 | } 45 | 46 | return res 47 | } 48 | 49 | try { 50 | const repoRes = await makeRequest("https://api.github.com/repos/upstash/jstack") 51 | const { stargazers_count } = (await repoRes.json()) as { stargazers_count: number } 52 | 53 | const lastPage = Math.ceil(stargazers_count / 20) 54 | const remainingItems = stargazers_count % 20 55 | const needExtraItems = remainingItems > 0 && remainingItems < 20 56 | 57 | let stargazers: Stargazer[] = [] 58 | 59 | if (needExtraItems) { 60 | try { 61 | const previousPageRes = await makeRequest( 62 | `https://api.github.com/repos/upstash/jstack/stargazers?per_page=20&page=${lastPage - 1}` 63 | ) 64 | 65 | const previousPageStargazers = (await previousPageRes.json()) as Stargazer[] 66 | stargazers = previousPageStargazers.slice(-(20 - remainingItems)) 67 | } catch (error) { 68 | console.error("[GitHub API Error] Failed to fetch previous page:", { 69 | page: lastPage - 1, 70 | error: error instanceof Error ? error.message : "Unknown error", 71 | timestamp: new Date().toISOString(), 72 | }) 73 | } 74 | } 75 | 76 | try { 77 | const lastPageRes = await makeRequest( 78 | `https://api.github.com/repos/upstash/jstack/stargazers?per_page=20&page=${lastPage}` 79 | ) 80 | 81 | const lastPageStargazers = (await lastPageRes.json()) as Stargazer[] 82 | stargazers = [...stargazers, ...lastPageStargazers].reverse() 83 | 84 | return { stargazers, stargazerCount: stargazers_count } 85 | } catch (error) { 86 | console.error("[GitHub API Error] Failed to fetch last page:", { 87 | page: lastPage, 88 | error: error instanceof Error ? error.message : "Unknown error", 89 | timestamp: new Date().toISOString(), 90 | }) 91 | throw error 92 | } 93 | } catch (error) { 94 | console.error("[GitHub API Error] Unhandled error:", { 95 | error: error instanceof Error ? error.message : "Unknown error", 96 | timestamp: new Date().toISOString(), 97 | }) 98 | 99 | return { stargazers: [], stargazerCount: 0 } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /www/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | code { 6 | @apply font-code; 7 | } 8 | 9 | code { 10 | counter-reset: step; 11 | counter-increment: step 0; 12 | font-size: 14px; 13 | } 14 | 15 | .shiki span { 16 | background-color: transparent !important; 17 | } 18 | 19 | code { 20 | @apply text-sm !leading-loose; 21 | } 22 | 23 | pre [data-line] { 24 | @apply px-4 border-l-2 border-l-transparent; 25 | } 26 | 27 | [data-highlighted-line] { 28 | background: rgba(255, 180, 40, 0.1); 29 | @apply !border-l-brand-400; 30 | } 31 | 32 | [data-highlighted-chars] { 33 | @apply bg-zinc-600/50 rounded; 34 | box-shadow: 0 0 0 4px rgb(82 82 91 / 0.5); 35 | } 36 | 37 | @layer base { 38 | [inert] ::-webkit-scrollbar { 39 | display: none; 40 | } 41 | 42 | :root { 43 | --shiki-color-text: theme("colors.white"); 44 | --shiki-token-constant: theme("colors.emerald.300"); 45 | --shiki-token-string: theme("colors.emerald.300"); 46 | --shiki-token-comment: theme("colors.zinc.500"); 47 | --shiki-token-keyword: theme("colors.sky.300"); 48 | --shiki-token-parameter: theme("colors.pink.300"); 49 | --shiki-token-function: theme("colors.violet.300"); 50 | --shiki-token-string-expression: theme("colors.emerald.300"); 51 | --shiki-token-punctuation: theme("colors.zinc.200"); 52 | 53 | --background: 0 0% 100%; 54 | --foreground: 224 71.4% 4.1%; 55 | --card: 0 0% 100%; 56 | --card-foreground: 224 71.4% 4.1%; 57 | --popover: 0 0% 100%; 58 | --popover-foreground: 224 71.4% 4.1%; 59 | --primary: 220.9 39.3% 11%; 60 | --primary-foreground: 210 20% 98%; 61 | --secondary: 220 14.3% 95.9%; 62 | --secondary-foreground: 220.9 39.3% 11%; 63 | 64 | --muted: 220 14.3% 95.9%; 65 | --muted-light: 60 100% 98% / 0.86; 66 | --muted-dark: 240 33% 94% / 0.6; 67 | --muted-foreground: 240 5% 64.9%; 68 | 69 | --accent: 220 14.3% 95.9%; 70 | --accent-foreground: 220.9 39.3% 11%; 71 | --destructive: 0 84.2% 60.2%; 72 | --destructive-foreground: 210 20% 98%; 73 | --border: 220 13% 91%; 74 | --input: 220 13% 91%; 75 | --ring: 224 71.4% 4.1%; 76 | --chart-1: 12 76% 61%; 77 | --chart-2: 173 58% 39%; 78 | --chart-3: 197 37% 24%; 79 | --chart-4: 43 74% 66%; 80 | --chart-5: 27 87% 67%; 81 | --radius: 0.5rem; 82 | } 83 | .dark { 84 | --background: 224 71.4% 4.1%; 85 | --foreground: 210 20% 98%; 86 | --card: 224 71.4% 4.1%; 87 | --card-foreground: 210 20% 98%; 88 | --popover: 224 71.4% 4.1%; 89 | --popover-foreground: 210 20% 98%; 90 | --primary: 210 20% 98%; 91 | --primary-foreground: 220.9 39.3% 11%; 92 | --secondary: 215 27.9% 16.9%; 93 | --secondary-foreground: 210 20% 98%; 94 | 95 | --muted: 220 14.3% 95.9%; 96 | --muted-light: 60 100% 98% / 0.86; 97 | --muted-dark: 240 33% 94% / 0.6; 98 | --muted-foreground: 240 5% 64.9%; 99 | 100 | --accent-foreground: 210 20% 98%; 101 | --destructive: 0 62.8% 30.6%; 102 | --destructive-foreground: 210 20% 98%; 103 | --border: 215 27.9% 16.9%; 104 | --input: 215 27.9% 16.9%; 105 | --ring: 216 12.2% 83.9%; 106 | --chart-1: 220 70% 50%; 107 | --chart-2: 160 60% 45%; 108 | --chart-3: 30 80% 55%; 109 | --chart-4: 280 65% 60%; 110 | --chart-5: 340 75% 55%; 111 | } 112 | } 113 | @layer base { 114 | * { 115 | @apply border-border; 116 | } 117 | body { 118 | @apply bg-background text-foreground; 119 | } 120 | } 121 | 122 | .typography { 123 | @apply text-gray-200 text-base/7; 124 | } 125 | -------------------------------------------------------------------------------- /www/src/app/docs/doc-navigation.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DOCS_CONFIG } from "@/config" 4 | import { allDocs } from "content-collections" 5 | import { usePathname } from "next/navigation" 6 | import Link from "next/link" 7 | import { cn } from "@/lib/utils" 8 | 9 | export function useDocNavigation() { 10 | const pathname = usePathname() 11 | 12 | const docsByCategory = Object.entries(DOCS_CONFIG.categories).reduce( 13 | (acc, [category, config]) => { 14 | const categoryDocs = allDocs.filter( 15 | (doc) => doc._meta.path.split("/")[0] === category, 16 | ) 17 | const sortedDocs = categoryDocs.sort((a, b) => { 18 | const aIndex = config.items.indexOf( 19 | a._meta.path.split("/")[1] as string, 20 | ) 21 | const bIndex = config.items.indexOf( 22 | b._meta.path.split("/")[1] as string, 23 | ) 24 | return aIndex - bIndex 25 | }) 26 | acc[category] = sortedDocs 27 | return acc 28 | }, 29 | {} as Record, 30 | ) 31 | 32 | const sortedCategories = Object.entries(docsByCategory).sort(([a], [b]) => { 33 | const aOrder = 34 | DOCS_CONFIG.categories[a as keyof typeof DOCS_CONFIG.categories]?.order ?? 35 | Infinity 36 | const bOrder = 37 | DOCS_CONFIG.categories[b as keyof typeof DOCS_CONFIG.categories]?.order ?? 38 | Infinity 39 | return aOrder - bOrder 40 | }) 41 | 42 | const isActiveLink = (path: string) => pathname === `/docs/${path}` 43 | 44 | return { 45 | sortedCategories, 46 | isActiveLink, 47 | } 48 | } 49 | 50 | interface DocNavigationProps { 51 | onLinkClick?: () => void 52 | className?: string 53 | } 54 | 55 | export function DocNavigation({ onLinkClick, className }: DocNavigationProps) { 56 | const { sortedCategories, isActiveLink } = useDocNavigation() 57 | 58 | return ( 59 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /www/src/components/stars-section.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { client } from "@/lib/client" 4 | import { cn } from "@/lib/utils" 5 | import { useQuery } from "@tanstack/react-query" 6 | import { Star } from "lucide-react" 7 | import { Icons } from "./icons" 8 | import { Stargazer, StargazerLoading, StargazerMore } from "./landing/stargazer" 9 | import { ShinyButton } from "./shiny-button" 10 | 11 | export const StarsSection = () => { 12 | const { data: stargazerInfo, isPending } = useQuery({ 13 | queryFn: async () => { 14 | const res = await client.stargazers.recent.$get() 15 | return await res.json() 16 | }, 17 | queryKey: ["stargazer-info"], 18 | }) 19 | 20 | return ( 21 |
22 |
23 |

30 | 31 | {" "} 32 | 33 | {stargazerInfo?.stargazerCount.toLocaleString() || "..."} devs 34 | 35 | 36 | 37 | {" "} 38 | shipping modern Next.js{" "} 39 | 40 | 41 |

42 | 43 |

44 | A real-time feed of the latest supporters - thanks for starring the 45 | GitHub repo! 46 |

47 | 48 |
49 |
50 | {isPending ? ( 51 | <> 52 | {Array.from({ length: 20 }).map((_, i) => ( 53 | 54 | ))} 55 | 56 | 57 | ) : ( 58 | <> 59 | {stargazerInfo?.stargazers.map((stargazer) => ( 60 | 65 | ))} 66 | 67 | 68 | 69 | )} 70 |
71 |
72 | 73 |
74 | 78 | 79 | Star on GitHub 80 | 81 | {stargazerInfo?.stargazerCount.toLocaleString() || "..."} 82 | 83 | 84 | 85 | 86 |

87 | Star the repo to appear above 🫶 88 |

89 |
90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /www/src/components/landing/hero-section.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import Link from "next/link" 3 | import { Icons } from "../icons" 4 | import { Check } from "lucide-react" 5 | 6 | export const HeroSection = () => { 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 |

14 | JStack 1.0 just released! 15 |

16 |
17 |

24 | 25 | Ship{" "} 26 | 27 | {" "} 28 | high-performance{" "} 29 | 30 | {" "} 31 | 32 | Next.js apps in minutes 33 |

34 | 35 |
36 |

37 | The stack for building seriously{" "} 38 | fast,{" "} 39 | lightweight and{" "} 40 | 41 | end-to-end typesafe{" "} 42 | Next.js apps. 43 | 44 |

45 | 46 |
    47 |
  • 48 | 49 | Incredible developer experience 50 |
  • 51 |
  • 52 | 53 | Automatic type-safety & autocompletion 54 |
  • 55 |
  • 56 | 57 | Deploy anywhere: Vercel, Cloudflare, etc. 58 |
  • 59 |
60 |
61 | 62 |
63 | 64 | 68 | Start Shipping Today → 69 | 70 | 71 |
72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /www/src/docs/introduction/jstack.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why JStack 3 | summary: This is my first post 4 | --- 5 | 6 | # About JStack 7 | 8 | **JStack is a TypeScript + Next.js stack built for maximum developer experience and application performance.** It builds on some of the most modern, high-quality pieces of software in 2025: 9 | 10 | - [**Hono 🔥**](https://hono.dev/) as a portable, lightweight Next.js backend 11 | - [**Zod 💎**](https://github.com/colinhacks/zod) for runtime validation 12 | - [**Drizzle ORM 💩**](https://orm.drizzle.team/) to interact with our database 13 | 14 | In the following section, I'll explain why these tools provide an incredible foundation for building fast, reliable, and production-ready Next.js applications. 15 | 16 | --- 17 | 18 | ## Why I created JStack 19 | 20 | Since I first tried the [T3 Stack](https://create.t3.gg/) a few years ago, I've loved the concept of type safety between client and server. 21 | 22 | Using TypeScript across the front- and backend gives you crazy confidence when writing code and prevents bugs before they happen. 23 | 24 | Combine that with the runtime safety provided by [Zod](https://github.com/colinhacks/zod), and you've got a safe and TypeScript-friendly way to build full-stack Next.js projects. 25 | 26 | However, over a few years of using the T3 stack for YouTube projects (both privately and for my production Next.js projects), **I wish the stack did a few things differently**. 27 | 28 | --- 29 | 30 | ### 1. Independent State Management 31 | 32 | tRPC is a _fantastic_ tool for providing type-safety between your frontend and backend. However, it has the trade-off of coupling itself to React Query hooks. This coupling is convenient but also limiting. As you get into more advanced use cases and eventually run into problems, I find myself having to find the "tRPC-specific" solution to the problem. 33 | 34 | Almost every question about React Query, every pattern you might want to know about, has already been answered. Often even by the (very active) maintainer [TKDodo himself](https://x.com/TkDodo): 35 | 36 | 37 | TKDodo, the maintainer of React Query aka TanStack Query 38 | 39 | 40 | The user base is so large that there are almost no unanswered questions. Best practices have been established over the years, and the tRPC implementation sometimes interferes with them. 41 | 42 | I also find myself unable to use React Query as an **incredible** standalone state manager because tRPC associates a frontend declaration with a backend procedure. 43 | 44 | JStack does not couple its type-safe client to any state manager. It works perfectly with React Query and any other state manager you might want to use (like Zustand, Jotai, and even Redux 🤮). Calling your client directly from a Zustand store outside of React scope? No problem, because your client is just a type-safe fetch wrapper. 45 | 46 | You can use all the standard React Query best practices and patterns and don't need to find tRPC-specific solutions to your problems. I like this no abstraction approach a lot more. 47 | 48 | --- 49 | 50 | ### 2. Not Just JSON Responses 51 | 52 | Because I built JStack on top of [Hono](https://hono.dev), which uses web standard responses under the hood, your API is not limited to JSON responses. JStack natively supports: 53 | 54 | - JSON 55 | - SuperJSON 56 | - Plain text 57 | - HTML 58 | - Web Standard Response 59 | 60 | I've never felt comfortable building anything other than app-internal APIs in tRPC. JStack routers follow an intuitive naming convention to serve both internal APIs and anything public-facing, all right from a single Next.js backend. 61 | 62 | --- 63 | 64 | ### 3. Platform Agnostic Deployment 65 | 66 | I recommend deploying to [Cloudflare Workers](https://workers.cloudflare.com/) because they are incredibly cheap, fast, and natively support long-lived WebSocket connections. However, JStack is built on top of [Hono](https://hono.dev), so you can deploy anywhere with minimal to no code changes: 67 | 68 | - Cloudflare 69 | - Vercel 70 | - Netlify 71 | - Railway 72 | - AWS 73 | - ... 74 | -------------------------------------------------------------------------------- /www/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | {/* 48 | 49 | Close 50 | */} 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /packages/jstack-shared/src/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { logger } from "./logger" 3 | 4 | type Schema = z.ZodTypeAny | undefined 5 | 6 | interface SchemaConfig { 7 | incomingSchema: Schema 8 | outgoingSchema: Schema 9 | } 10 | 11 | export class EventEmitter { 12 | eventHandlers = new Map any)[]>() 13 | ws: WebSocket 14 | 15 | incomingSchema: Schema 16 | outgoingSchema: Schema 17 | 18 | constructor(ws: WebSocket, schemas: SchemaConfig) { 19 | const { incomingSchema, outgoingSchema } = schemas 20 | 21 | this.ws = ws 22 | this.incomingSchema = incomingSchema 23 | this.outgoingSchema = outgoingSchema 24 | } 25 | 26 | emit(event: string, data: any): boolean { 27 | if (this.ws.readyState !== WebSocket.OPEN) { 28 | logger.warn("WebSocket is not in OPEN state. Message not sent.") 29 | return false 30 | } 31 | 32 | if (this.outgoingSchema) { 33 | try { 34 | this.outgoingSchema.parse(data) 35 | } catch (err) { 36 | this.handleSchemaMismatch(event, data, err) 37 | return false 38 | } 39 | } 40 | 41 | this.ws.send(JSON.stringify([event, data])) 42 | return true 43 | } 44 | 45 | handleSchemaMismatch(event: string, data: any, err: any) { 46 | if (err instanceof z.ZodError) { 47 | logger.error(`Invalid outgoing event data for "${event}":`, { 48 | errors: err.errors 49 | .map((e) => `${e.path.join(".")}: ${e.message}`) 50 | .join(", "), 51 | data: JSON.stringify(data, null, 2), 52 | }) 53 | } else { 54 | logger.error(`Error validating outgoing event "${event}":`, err) 55 | } 56 | } 57 | 58 | handleEvent(eventName: string, data: any) { 59 | const handlers = this.eventHandlers.get(eventName) 60 | 61 | if (!handlers?.length) { 62 | logger.warn( 63 | `No handlers registered for event "${eventName}". Did you forget to call .on("${eventName}", handler)?` 64 | ) 65 | return 66 | } 67 | 68 | let validatedData = data 69 | if (this.incomingSchema) { 70 | try { 71 | validatedData = this.incomingSchema.parse(data) 72 | } catch (err) { 73 | if (err instanceof z.ZodError) { 74 | logger.error(`Invalid incoming event data for "${eventName}":`, { 75 | errors: err.errors 76 | .map((e) => `${e.path.join(".")}: ${e.message}`) 77 | .join(", "), 78 | data: JSON.stringify(data, null, 2), 79 | }) 80 | } else { 81 | logger.error(`Error validating incoming event "${eventName}":`, err) 82 | } 83 | return 84 | } 85 | } 86 | 87 | let hasErrors = false 88 | handlers.forEach((handler, index) => { 89 | try { 90 | handler(validatedData) 91 | } catch (err) { 92 | hasErrors = true 93 | const error = err instanceof Error ? err : new Error(String(err)) 94 | logger.error( 95 | `Error in handler ${index + 1}/${handlers.length} for event "${eventName}":`, 96 | { 97 | error: error.message, 98 | stack: error.stack, 99 | data: JSON.stringify(validatedData, null, 2), 100 | } 101 | ) 102 | } 103 | }) 104 | 105 | if (hasErrors) { 106 | throw new Error( 107 | `One or more handlers failed for event "${eventName}". Check logs for details.` 108 | ) 109 | } 110 | } 111 | 112 | off(event: string, callback?: (data: any) => any) { 113 | if (!callback) { 114 | this.eventHandlers.delete(event as string) 115 | } else { 116 | const handlers = this.eventHandlers.get(event as string) 117 | if (handlers) { 118 | const index = handlers.indexOf(callback) 119 | if (index !== -1) { 120 | handlers.splice(index, 1) 121 | if (handlers.length === 0) { 122 | this.eventHandlers.delete(event as string) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | on(event: string, callback?: (data: any) => any): void { 130 | if (!callback) { 131 | logger.error( 132 | `No callback provided for event handler "${event.toString()}". Ppass a callback to handle this event.` 133 | ) 134 | 135 | return 136 | } 137 | 138 | const handlers = this.eventHandlers.get(event as string) || [] 139 | handlers.push(callback) 140 | this.eventHandlers.set(event as string, handlers) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /packages/jstack/src/server/types.ts: -------------------------------------------------------------------------------- 1 | import { Context, TypedResponse } from "hono" 2 | import type superjson from "superjson" 3 | import { z } from "zod" 4 | 5 | import { Env, Input } from "hono/types" 6 | import { StatusCode } from "hono/utils/http-status" 7 | import { ServerSocket } from "jstack-shared" 8 | import { IO } from "./io" 9 | 10 | type SuperJSONParsedType = ReturnType> 11 | 12 | export type SuperJSONTypedResponse< 13 | T, 14 | U extends StatusCode = StatusCode, 15 | > = TypedResponse, U, "json"> 16 | 17 | export interface RouterConfig { 18 | name?: string 19 | } 20 | 21 | export type SuperJSONHandler = { 22 | superjson: (data: T, status?: number) => SuperJSONTypedResponse 23 | } 24 | 25 | export type ContextWithSuperJSON< 26 | E extends Env = any, 27 | P extends string = any, 28 | I extends Input = {}, 29 | > = Context & SuperJSONHandler 30 | 31 | export type InferMiddlewareOutput = 32 | T extends MiddlewareFunction ? R : unknown 33 | 34 | export type MiddlewareFunction< 35 | T = {}, 36 | R = void, 37 | E extends Env = any, 38 | > = (params: { 39 | ctx: T 40 | next: (args?: B) => Promise 41 | c: ContextWithSuperJSON 42 | }) => Promise 43 | 44 | export type EmitFunction = (event: string, data?: any) => Promise 45 | export type RoomEmitFunction = (room: string, data?: any) => Promise 46 | 47 | export type WebSocketHandler = { 48 | onConnect?: ({ 49 | socket, 50 | }: { 51 | socket: ServerSocket 52 | }) => any 53 | onDisconnect?: ({ 54 | socket, 55 | }: { 56 | socket: ServerSocket 57 | }) => any 58 | onError?: ({ 59 | socket, 60 | error, 61 | }: { 62 | socket: ServerSocket 63 | error: Event 64 | }) => any 65 | } 66 | 67 | export type WebSocketOperation< 68 | // FIXME: Record type error 69 | IncomingSchema extends Record, 70 | OutgoingSchema extends Record, 71 | E extends Env = any, 72 | > = { 73 | type: "ws" 74 | incoming?: IncomingSchema 75 | outgoing?: OutgoingSchema 76 | outputFormat: "ws" 77 | handler: ({ 78 | io, 79 | c, 80 | ctx, 81 | }: { 82 | io: IO 83 | c: ContextWithSuperJSON 84 | ctx: Input 85 | }) => OptionalPromise> 86 | middlewares: MiddlewareFunction[] 87 | } 88 | 89 | export type ResponseType = 90 | | SuperJSONTypedResponse 91 | | TypedResponse 92 | | Response 93 | | void 94 | 95 | type UnwrapResponse = 96 | Awaited extends TypedResponse 97 | ? U 98 | : Awaited extends SuperJSONTypedResponse 99 | ? U 100 | : Awaited extends Response 101 | ? any 102 | : Awaited extends void 103 | ? void 104 | : T 105 | 106 | export type GetOperation< 107 | Schema extends Record | void, 108 | Return = OptionalPromise>, 109 | E extends Env = any, 110 | > = { 111 | type: "get" 112 | schema?: z.ZodType | void 113 | handler: ({ 114 | c, 115 | ctx, 116 | input, 117 | }: { 118 | ctx: Input 119 | c: ContextWithSuperJSON 120 | input: Schema extends Record ? Schema : void 121 | }) => UnwrapResponse> 122 | middlewares: MiddlewareFunction[] 123 | } 124 | 125 | type OptionalPromise = T | Promise 126 | 127 | export type PostOperation< 128 | Schema extends Record | void, 129 | Return = OptionalPromise>, 130 | E extends Env = any, 131 | > = { 132 | type: "post" 133 | schema?: z.ZodType | void 134 | handler: ({ 135 | ctx, 136 | c, 137 | }: { 138 | ctx: Input 139 | c: ContextWithSuperJSON 140 | input: Schema extends Record ? Schema : void 141 | }) => UnwrapResponse> 142 | middlewares: MiddlewareFunction[] 143 | } 144 | 145 | export type OperationType< 146 | I extends Record, 147 | O extends Record, 148 | E extends Env = any, 149 | > = 150 | | GetOperation 151 | | PostOperation 152 | | WebSocketOperation 153 | 154 | export type InferInput = 155 | T extends OperationType 156 | ? I extends z.ZodTypeAny 157 | ? z.infer 158 | : I 159 | : void 160 | -------------------------------------------------------------------------------- /packages/jstack/src/server/j.ts: -------------------------------------------------------------------------------- 1 | import { cors } from "hono/cors" 2 | import { HTTPException } from "hono/http-exception" 3 | import { Env, HTTPResponseError, MiddlewareHandler } from "hono/types" 4 | import { ContentfulStatusCode } from "hono/utils/http-status" 5 | import { ZodError } from "zod" 6 | import { mergeRouters } from "./merge-routers" 7 | import { Procedure } from "./procedure" 8 | import { Router } from "./router" 9 | import { MiddlewareFunction, OperationType } from "./types" 10 | 11 | const router = < 12 | T extends Record>, 13 | E extends Env, 14 | >( 15 | procedures: T = {} as T 16 | ): Router => { 17 | return new Router(procedures) 18 | } 19 | 20 | /** 21 | * Adapts a Hono middleware to be compatible with the type-safe middleware format 22 | */ 23 | export function fromHono( 24 | honoMiddleware: MiddlewareHandler 25 | ): MiddlewareFunction { 26 | return async ({ c, next }) => { 27 | await honoMiddleware(c, async () => { 28 | const result = await next() 29 | return result 30 | }) 31 | } 32 | } 33 | 34 | class JStack { 35 | init() { 36 | return { 37 | /** 38 | * Type-safe router factory function that creates a new router instance. 39 | * 40 | * @template T - Record of operation types (get/post/websockets) 41 | * @template E - Environment type for the router 42 | * @returns {Router} A new router instance with type-safe procedure definitions 43 | * 44 | * @example 45 | * const userRouter = router({ 46 | * getUser: publicProcedure 47 | * .input(z.object({ id: z.string() })) 48 | * .get(async ({ input }) => { 49 | * return { id: input.id, name: "John Doe" } 50 | * }), 51 | * 52 | * createUser: publicProcedure 53 | * .input(z.object({ name: z.string() })) 54 | * .post(async ({ input }) => { 55 | * return { id: "123", name: input.name } 56 | * }) 57 | * }) 58 | */ 59 | router, 60 | mergeRouters, 61 | middleware: ( 62 | middleware: MiddlewareFunction 63 | ): MiddlewareFunction => middleware, 64 | fromHono, 65 | procedure: new Procedure(), 66 | defaults: { 67 | /** 68 | * CORS middleware configuration with default settings for API endpoints. 69 | * 70 | * @default 71 | * - Allows 'x-is-superjson' and 'Content-Type' in headers 72 | * - Exposes 'x-is-superjson' in headers 73 | * - Accepts all origins 74 | * - Enables credentials 75 | */ 76 | cors: cors({ 77 | allowHeaders: ["x-is-superjson", "Content-Type"], 78 | exposeHeaders: ["x-is-superjson"], 79 | origin: (origin) => origin, 80 | credentials: true, 81 | }), 82 | /** 83 | * Global error handler for API endpoints. 84 | * 85 | * @example 86 | * // Client-side error handling 87 | * const { mutate } = useMutation({ 88 | * onError: (err: HTTPException) => { 89 | * if (err.status === 401) { 90 | * console.log(err.message) // Handle unauthorized 91 | * } 92 | * } 93 | * }) 94 | */ 95 | errorHandler: (err: Error | HTTPResponseError) => { 96 | console.error("[API Error]", err) 97 | 98 | if (err instanceof HTTPException) { 99 | return err.getResponse() 100 | } else if (err instanceof ZodError) { 101 | const httpError = new HTTPException(422, { 102 | message: "Validation error", 103 | cause: err, 104 | }) 105 | 106 | return httpError.getResponse() 107 | } else if ("status" in err && typeof err.status === "number") { 108 | const httpError = new HTTPException( 109 | err.status as ContentfulStatusCode, 110 | { 111 | message: err.message || "API Error", 112 | cause: err, 113 | } 114 | ) 115 | 116 | return httpError.getResponse() 117 | } else { 118 | const httpError = new HTTPException(500, { 119 | message: 120 | "An unexpected error occurred. Check server logs for details.", 121 | cause: err, 122 | }) 123 | 124 | return httpError.getResponse() 125 | } 126 | }, 127 | }, 128 | } 129 | } 130 | } 131 | 132 | export const jstack = new JStack() 133 | -------------------------------------------------------------------------------- /www/src/docs/backend/middleware.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middleware 3 | summary: Understanding and implementing middleware in JStack 4 | --- 5 | 6 | # Middleware 7 | 8 | Middleware in JStack allows you to add reusable logic between your procedure requests and handlers. It's perfect for cross-cutting concerns like authentication, logging, or error handling. 9 | 10 | --- 11 | 12 | ## Basic Middleware Structure 13 | 14 | ```ts server/jstack.ts 15 | const myMiddleware = j.middleware(async ({ c, next }) => { 16 | // 1️⃣ Code that runs before the handler 17 | // ... 18 | 19 | // 2️⃣ Pass data to the next middleware/handler 20 | return await next({ customData: "value" }) 21 | }) 22 | ``` 23 | 24 | --- 25 | 26 | ## Common Use Cases 27 | 28 | ### Authentication Middleware 29 | 30 | The following middleware authenticates a user. When our procedure runs, we can be 100% sure that this user really exists, because otherwise the middleware will throw an error and prevent the procedure from running: 31 | 32 | ```ts server/jstack.ts {10-22, 29} 33 | import { HTTPException } from "hono/http-exception" 34 | import { jstack } from "jstack" 35 | 36 | interface Env { 37 | Bindings: { DATABASE_URL: string } 38 | } 39 | 40 | export const j = jstack.init() 41 | 42 | const authMiddleware = j.middleware(async ({ c, next }) => { 43 | // Mocked user authentication check... 44 | const isAuthenticated = true 45 | 46 | if (!isAuthenticated) { 47 | throw new HTTPException(401, { 48 | message: "Unauthorized, sign in to continue.", 49 | }) 50 | } 51 | 52 | // 👇 Attach user to `ctx` object 53 | return await next({ user: { name: "John Doe" } }) 54 | }) 55 | 56 | /** 57 | * Public (unauthenticated) procedures 58 | * This is the base piece you use to build new procedures. 59 | */ 60 | export const publicProcedure = j.procedure 61 | export const privateProcedure = publicProcedure.use(authMiddleware) 62 | ``` 63 | 64 | On any `privateProcedure` we can now safely access the user object: 65 | 66 | ```ts server/routers/post-router.ts {5-6} 67 | import { j, privateProcedure } from "../jstack" 68 | 69 | export const postRouter = j.router({ 70 | list: privateProcedure.get(({ c, ctx }) => { 71 | // 👇 Access middleware data through ctx 72 | const { user } = ctx 73 | 74 | return c.json({ posts: [] }) 75 | }), 76 | }) 77 | ``` 78 | 79 | --- 80 | 81 | ## Middleware Chaining 82 | 83 | Chain multiple middlewares using `.use()`: 84 | 85 | ```ts 86 | const enhancedProcedure = publicProcedure 87 | .use(authMiddleware) 88 | .use(loggingMiddleware) 89 | .use(rateLimitMiddleware) 90 | ``` 91 | 92 | If you have multiple middlewares that depend on each other, by definition JStack cannot know the order in which they were run. Therefore, use the type inference utility: 93 | 94 | ```ts server/jstack.ts {14, 18} 95 | import { InferMiddlewareOutput, jstack } from "jstack" 96 | 97 | interface Env { 98 | Bindings: { DATABASE_URL: string } 99 | } 100 | 101 | export const j = jstack.init() 102 | 103 | // 1️⃣ Auth middleware runs first 104 | const authMiddleware = j.middleware(async ({ c, next }) => { 105 | return await next({ user: { name: "John Doe" } }) 106 | }) 107 | 108 | type AuthMiddlewareOutput = InferMiddlewareOutput 109 | 110 | // 2️⃣ Logging middleware runs second 111 | const loggingMiddleware = j.middleware(async ({ c, ctx, next }) => { 112 | const { user } = ctx as AuthMiddlewareOutput 113 | 114 | const start = performance.now() 115 | await next() 116 | const end = performance.now() 117 | 118 | console.log(`${user.name}'s request took ${end - start}ms`) 119 | }) 120 | 121 | /** 122 | * Public (unauthenticated) procedures 123 | * This is the base piece you use to build new procedures. 124 | */ 125 | export const publicProcedure = j.procedure 126 | export const privateProcedure = publicProcedure 127 | .use(authMiddleware) 128 | .use(loggingMiddleware) 129 | ``` 130 | 131 | --- 132 | 133 | ## Using Hono Middleware 134 | 135 | JStack is compatible with Hono middleware via the `fromHono` adapter: 136 | 137 | ```ts 138 | import { j } from "./jstack" 139 | import { cors } from "hono/cors" 140 | 141 | const corsMiddleware = j.fromHono(cors()) 142 | const procedureWithCors = publicProcedure.use(corsMiddleware) 143 | ``` 144 | 145 | --- 146 | 147 | ## Best Practices 148 | 149 | - Keep middleware focused on a single responsibility 150 | - Handle errors via your `appRouter`'s `onError()` 151 | 152 | --- 153 | 154 | ## Common Middleware Examples 155 | 156 | - Authentication 157 | - Request logging 158 | - Rate limiting 159 | - Error handling 160 | - Request validation 161 | - Performance monitoring 162 | - CORS handling 163 | 164 | --- 165 | 166 | → To see all built-in Hono middleware, check out the [Hono middleware documentation](https://hono.dev/docs/middleware/builtin/basic-auth). 167 | --------------------------------------------------------------------------------