Init commit

This commit is contained in:
2025-10-18 00:31:23 +01:00
commit fcd081c1c5
30 changed files with 4912 additions and 0 deletions

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Drizzle
DATABASE_URL="file:./db.sqlite"
# Example:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
db.sqlite
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# idea files
.idea

19
README.md Normal file
View File

@@ -0,0 +1,19 @@
## Drizzle
`npm run db:generate` to create any migration files from the schema
`npm run db:migrate` to actually apply the migrations to the db
`npm run db:push` to basically push straight from schema to db
## What's included?
- [Next.js](https://nextjs.org)
- [Prisma](https://prisma.io)
- [Drizzle](https://orm.drizzle.team)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
- [biome](https://biomejs.dev)
- [better-auth](https://www.better-auth.com/)
## T3
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.

26
biome.jsonc Normal file
View File

@@ -0,0 +1,26 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": { "ignoreUnknown": false, "ignore": [] },
"formatter": { "enabled": true },
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"nursery": {
"useSortedClasses": {
"level": "warn",
"fix": "safe",
"options": {
"functions": ["clsx", "cva", "cn"]
}
}
},
"recommended": true
}
}
}

2
docker/.env.authentik Normal file
View File

@@ -0,0 +1,2 @@
PG_PASS=tqEJ1Hp2j6Z0WsHhhfkUCEfLRmFII6xu761k4CD1ZMQA93lm
AUTHENTIK_SECRET_KEY=dbHWpMFDVSUMqCuu1pmGJi2qV/8f61pnHlySudK+/pbCpb3RBZI8pw8nhkIGWvXH1lcuyNq5bt8LI8d3

View File

@@ -0,0 +1,106 @@
name: authentik
x-logging: &default-logging
options:
max-size: 32m
max-file: 4
services:
authentik-postgresql:
container_name: authentik-postgresql
image: docker.io/library/postgres:16.8-alpine3.21
restart: unless-stopped
env_file:
- .env
- .env.authentik
logging: *default-logging
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
environment:
POSTGRES_PASSWORD: ${AUTHENTIK_PG_PASS:?database password required}
POSTGRES_USER: authentik
POSTGRES_DB: authentik
volumes:
- ./authentik/postgres:/var/lib/postgresql/data
authentik-redis:
container_name: authentik-redis
image: docker.io/library/redis:8.2.1-alpine3.22
restart: unless-stopped
env_file:
- .env
- .env.authentik
logging: *default-logging
command: --save 60 1 --loglevel warning
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- ./authentik/redis:/data
authentik-server:
container_name: authentik-server
image: ghcr.io/goauthentik/server:2025.8.1
restart: unless-stopped
env_file:
- .env
- .env.authentik
logging: *default-logging
depends_on:
authentik-postgresql:
condition: service_healthy
authentik-redis:
condition: service_healthy
command: server
environment:
AUTHENTIK_REDIS__HOST: authentik-redis
AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS}
volumes:
- ./authentik/custom-templates:/templates
labels:
- traefik.enable=true
- traefik.http.routers.authentik.entryPoints=https
- traefik.http.routers.authentik.rule=Host(`authentik.home.joemonk.co.uk`) || (HostRegexp(`{subdomain:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?}.home.joemonk.co.uk`) && PathPrefix(`/outpost.goauthentik.io/`))
- traefik.http.routers.authentik.tls=true
- traefik.http.routers.authentik.tls.certresolver=letsencrypt
- traefik.http.routers.authentik.tls.domains[0].main=authentik.home.joemonk.co.uk
- traefik.http.routers.authentik.service=authentik
- traefik.http.services.authentik.loadbalancer.server.port=9000
- traefik.http.middlewares.authentik-traefik.forwardAuth.address=http:/authentik-server:9000/outpost.goauthentik.io/auth/traefik
- traefik.http.middlewares.authentik-traefik.forwardAuth.trustForwardHeader=true
- traefik.http.middlewares.authentik-traefik.forwardAuth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
authentik-worker:
container_name: authentik-worker
image: ghcr.io/goauthentik/server:2025.8.1
restart: unless-stopped
env_file:
- .env
- .env.authentik
logging: *default-logging
depends_on:
authentik-postgresql:
condition: service_healthy
authentik-redis:
condition: service_healthy
command: worker
user: root
environment:
AUTHENTIK_REDIS__HOST: authentik-redis
AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./authentik/certs:/certs
- ./authentik/custom-templates:/templates

View File

@@ -0,0 +1,73 @@
x-logging: &default-logging
options:
max-size: 32m
max-file: 4
services:
traefik:
container_name: traefik
image: traefik:3.5.1
restart: unless-stopped
env_file: .env
logging: *default-logging
depends_on:
- authelia
command:
- --api.insecure=true
- --providers.docker=true
- --providers.docker.watch=true
- --providers.docker.exposedbydefault=false
- --providers.file.directory=/config
- --providers.file.watch=true
- --accesslog
- --accesslog.format=json
- --entryPoints.http.address=:80
- --entryPoints.http.forwardedHeaders.trustedIPs=10.0.0.0/8,172.16.0.0/14,192.168.0.0/16,fc00::/7
- --entryPoints.http.proxyProtocol.trustedIPs=192.168.0.0/16
- --entryPoints.http.forwardedHeaders.insecure=false
- --entryPoints.http.proxyProtocol.insecure=false
- --entryPoints.https=true
- --entryPoints.https.address=:443
- --entryPoints.https.forwardedHeaders.trustedIPs=10.0.0.0/8,172.16.0.0/14,192.168.0.0/16,fc00::/7
- --entryPoints.https.proxyProtocol.trustedIPs=192.168.0.0/16
- --entryPoints.https.forwardedHeaders.insecure=false
- --entryPoints.https.proxyProtocol.insecure=false
- --entryPoints.http.http.redirections.entrypoint.to=https
- --entryPoints.http.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.letsencrypt
- --certificatesresolvers.letsencrypt.acme.storage=acme.json
- --certificatesresolvers.letsencrypt.acme.email=joemonk@hotmail.co.uk
- --certificatesresolvers.letsencrypt.acme.dnsChallenge.provider=route53
# Uncomment to use the staging env for testing volumes etc
- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
ports:
- 80:80
- 443:443
- 8080:8080
volumes:
- /mnt/cache/appdata/traefik/config:/config
- /mnt/user/appdata/traefik/letsencrypt/acme.json:/acme.json
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- traefik.enable=true
- traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth
- traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true
- traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email
- traefik.http.routers.traefik.entryPoints=https
- traefik.http.routers.traefik.rule=Host(`traefik.home.joemonk.co.uk`)
- traefik.http.routers.traefik.tls=true
- traefik.http.routers.traefik.tls.certresolver=letsencrypt
- traefik.http.routers.traefik.tls.domains[0].main=traefik.home.joemonk.co.uk
- traefik.http.routers.traefik.service=traefik
- traefik.http.routers.traefik.middlewares=authentik-traefik@docker
- traefik.http.services.traefik.loadbalancer.server.port=8080

12
drizzle.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { Config } from "drizzle-kit";
import { env } from "@/env";
export default {
schema: "./src/server/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: env.DATABASE_URL,
},
tablesFilter: ["cattery_*"],
} satisfies Config;

10
next.config.js Normal file
View File

@@ -0,0 +1,10 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
import "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {};
export default config;

3934
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "cattery",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"check": "biome check .",
"check:unsafe": "biome check --write --unsafe .",
"check:write": "biome check --write .",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "next dev --turbo",
"preview": "next build && next start",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@libsql/client": "^0.15.15",
"@t3-oss/env-nextjs": "^0.13.8",
"@tanstack/react-query": "^5.90.2",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"better-auth": "^1.3.27",
"drizzle-orm": "^0.44.6",
"next": "^15.5.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"server-only": "^0.0.1",
"superjson": "^2.2.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@biomejs/biome": "2.2.5",
"@tailwindcss/postcss": "^4.1.14",
"@types/node": "^24.7.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"drizzle-kit": "^0.31.5",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3"
},
"ct3aMetadata": {
"initVersion": "7.39.3"
},
"packageManager": "npm@11.6.0"
}

5
postcss.config.js Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,50 @@
"use client";
import { useState } from "react";
import { api } from "@/trpc/react";
export function LatestPost() {
const [latestPost] = api.post.getLatest.useSuspenseQuery();
const utils = api.useUtils();
const [name, setName] = useState("");
const createPost = api.post.create.useMutation({
onSuccess: async () => {
await utils.post.invalidate();
setName("");
},
});
return (
<div className="w-full max-w-xs">
{latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p>
) : (
<p>You have no posts yet.</p>
)}
<form
onSubmit={(e) => {
e.preventDefault();
createPost.mutate({ name });
}}
className="flex flex-col gap-2"
>
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-full bg-white/10 px-4 py-2 text-white"
/>
<button
type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending}
>
{createPost.isPending ? "Submitting..." : "Submit"}
</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,4 @@
import { auth } from "@/server/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);

View File

@@ -0,0 +1,34 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import type { NextRequest } from "next/server";
import { env } from "@/env";
import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
/**
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
* handling a HTTP request (e.g. when you make requests from Client Components).
*/
const createContext = async (req: NextRequest) => {
return createTRPCContext({
headers: req.headers,
});
};
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => createContext(req),
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});
export { handler as GET, handler as POST };

29
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,29 @@
import "@/styles/globals.css";
import type { Metadata } from "next";
import { Geist } from "next/font/google";
import { TRPCReactProvider } from "@/trpc/react";
export const metadata: Metadata = {
title: "Create T3 App",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geist = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
});
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${geist.variable}`}>
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}

53
src/app/page.tsx Normal file
View File

@@ -0,0 +1,53 @@
import Link from "next/link";
import { LatestPost } from "@/app/_components/post";
import { HydrateClient, api } from "@/trpc/server";
export default async function Home() {
const hello = await api.post.hello({ text: "from tRPC" });
void api.post.getLatest.prefetch();
return (
<HydrateClient>
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
<h1 className="font-extrabold text-5xl tracking-tight sm:text-[5rem]">
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
</h1>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
href="https://create.t3.gg/en/usage/first-steps"
target="_blank"
>
<h3 className="font-bold text-2xl">First Steps </h3>
<div className="text-lg">
Just the basics - Everything you need to know to set up your
database and authentication.
</div>
</Link>
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
href="https://create.t3.gg/en/introduction"
target="_blank"
>
<h3 className="font-bold text-2xl">Documentation </h3>
<div className="text-lg">
Learn more about Create T3 App, the libraries it uses, and how
to deploy it.
</div>
</Link>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-2xl text-white">
{hello ? hello.greeting : "Loading tRPC query..."}
</p>
</div>
<LatestPost />
</div>
</main>
</HydrateClient>
);
}

44
src/env.js Normal file
View File

@@ -0,0 +1,44 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
DATABASE_URL: z.string().url(),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
},
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
* useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
/**
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
* `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
});

23
src/server/api/root.ts Normal file
View File

@@ -0,0 +1,23 @@
import { postRouter } from "@/server/api/routers/post";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
post: postRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
/**
* Create a server-side caller for the tRPC API.
* @example
* const trpc = createCaller(createContext);
* const res = await trpc.post.all();
* ^? Post[]
*/
export const createCaller = createCallerFactory(appRouter);

View File

@@ -0,0 +1,30 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "@/server/api/trpc";
import { posts } from "@/server/db/schema";
export const postRouter = createTRPCRouter({
hello: publicProcedure
.input(z.object({ text: z.string() }))
.query(({ input }) => {
return {
greeting: `Hello ${input.text}`,
};
}),
create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(posts).values({
name: input.name,
});
}),
getLatest: publicProcedure.query(async ({ ctx }) => {
const post = await ctx.db.query.posts.findFirst({
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
});
return post ?? null;
}),
});

106
src/server/api/trpc.ts Normal file
View File

@@ -0,0 +1,106 @@
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { db } from "@/server/db";
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
* wrap this and provides the required context.
*
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
return {
db,
...opts,
};
};
/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* Create a server-side caller.
*
* @see https://trpc.io/docs/server/server-side-calls
*/
export const createCallerFactory = t.createCallerFactory;
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
/**
* Middleware for timing procedure execution and adding an artificial delay in development.
*
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
* network latency that would occur in production but not in local development.
*/
const timingMiddleware = t.middleware(async ({ next, path }) => {
const start = Date.now();
if (t._config.isDev) {
// artificial delay in dev
const waitMs = Math.floor(Math.random() * 400) + 100;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
const result = await next();
const end = Date.now();
console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
return result;
});
/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = t.procedure.use(timingMiddleware);

11
src/server/auth.ts Normal file
View File

@@ -0,0 +1,11 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/server/db"; // your drizzle instance
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite", // or "mysql", "sqlite"
}),
plugins: [nextCookies()] // make nextCookies is the last plugin in the array
});

19
src/server/db/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import { type Client, createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { env } from "@/env";
import * as schema from "./schema";
/**
* Cache the database connection in development. This avoids creating a new connection on every HMR
* update.
*/
const globalForDb = globalThis as unknown as {
client: Client | undefined;
};
export const client =
globalForDb.client ?? createClient({ url: env.DATABASE_URL });
if (env.NODE_ENV !== "production") globalForDb.client = client;
export const db = drizzle(client, { schema });

27
src/server/db/schema.ts Normal file
View File

@@ -0,0 +1,27 @@
// Example model schema from the Drizzle docs
// https://orm.drizzle.team/docs/sql-schema-declaration
import { sql } from "drizzle-orm";
import { index, sqliteTableCreator } from "drizzle-orm/sqlite-core";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
*
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
export const createTable = sqliteTableCreator((name) => `cattery_${name}`);
export const posts = createTable(
"post",
(d) => ({
id: d.integer({ mode: "number" }).primaryKey({ autoIncrement: true }),
name: d.text({ length: 256 }),
createdAt: d
.integer({ mode: "timestamp" })
.default(sql`(unixepoch())`)
.notNull(),
updatedAt: d.integer({ mode: "timestamp" }).$onUpdate(() => new Date()),
}),
(t) => [index("name_idx").on(t.name)],
);

6
src/styles/globals.css Normal file
View File

@@ -0,0 +1,6 @@
@import "tailwindcss";
@theme {
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

25
src/trpc/query-client.ts Normal file
View File

@@ -0,0 +1,25 @@
import {
QueryClient,
defaultShouldDehydrateQuery,
} from "@tanstack/react-query";
import SuperJSON from "superjson";
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
});

78
src/trpc/react.tsx Normal file
View File

@@ -0,0 +1,78 @@
"use client";
import { type QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchStreamLink, loggerLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { useState } from "react";
import SuperJSON from "superjson";
import type { AppRouter } from "@/server/api/root";
import { createQueryClient } from "./query-client";
let clientQueryClientSingleton: QueryClient | undefined = undefined;
const getQueryClient = () => {
if (typeof window === "undefined") {
// Server: always make a new query client
return createQueryClient();
}
// Browser: use singleton pattern to keep the same query client
clientQueryClientSingleton ??= createQueryClient();
return clientQueryClientSingleton;
};
export const api = createTRPCReact<AppRouter>();
/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;
export function TRPCReactProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
httpBatchStreamLink({
transformer: SuperJSON,
url: `${getBaseUrl()}/api/trpc`,
headers: () => {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
{props.children}
</api.Provider>
</QueryClientProvider>
);
}
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}

30
src/trpc/server.ts Normal file
View File

@@ -0,0 +1,30 @@
import "server-only";
import { createHydrationHelpers } from "@trpc/react-query/rsc";
import { headers } from "next/headers";
import { cache } from "react";
import { type AppRouter, createCaller } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
import { createQueryClient } from "./query-client";
/**
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
* handling a tRPC call from a React Server Component.
*/
const createContext = cache(async () => {
const heads = new Headers(await headers());
heads.set("x-trpc-source", "rsc");
return createTRPCContext({
headers: heads,
});
});
const getQueryClient = cache(createQueryClient);
const caller = createCaller(createContext);
export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient,
);

42
tsconfig.json Normal file
View File

@@ -0,0 +1,42 @@
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"checkJs": true,
/* Bundled projects */
"lib": ["dom", "dom.iterable", "ES2022"],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"incremental": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.js",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}