create-ej-app 15.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Ekansh Jain
3
+ Copyright (c) 2026 Ekansh Jain
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,5 +1,257 @@
1
1
  # create-ej-app
2
2
 
3
+ A no-bullshit CLI to bootstrap a production-ready **fucking beast** of a app with Next.js, Better Auth, Drizzle ORM, Elysia, Workflow, shadcn/ui, React Email, OTEL, and deployment wired up so you don’t screw your shit up in production.
4
+
5
+ ```bash
6
+ bunx create-ej-app@latest
3
7
  ```
4
- npx create-ej-app@latest
8
+
9
+ ---
10
+
11
+ ## ✨ Features (aka the good shit)
12
+
13
+ - **Next.js 16** – Powered by React 19, Server Components, App Router. Fast as hell.
14
+
15
+ - **Better Auth** – Auth that doesn’t suck:
16
+ - Email/Password
17
+ - Magic link (because passwords are annoying as fuck)
18
+ - OAuth with GitHub and Google
19
+ - Account linking
20
+ - Admin powers
21
+
22
+ - **Drizzle ORM** – Type-safe SQL over PostgreSQL without the usual ORM garbage.
23
+
24
+ - **Elysia** – Built for Bun. Minimal, fast, no nonsense.
25
+
26
+ - **Workflow** – Durable jobs so your background tasks don’t randomly die like weak shit.
27
+
28
+ - **React Email** – Build transactional emails using React components instead of ugly HTML nightmares.
29
+
30
+ - **OTEL (OpenTelemetry)** – Production-grade observability:
31
+ - Distributed tracing
32
+ - Metrics
33
+ - Structured logs
34
+ - Works with GCP, Datadog, Honeycomb, Grafana, etc.
35
+
36
+ - **shadcn/ui** – Clean, accessible components built on Radix UI.
37
+
38
+ - **TailwindCSS 4** – Utility-first styling with zero CSS drama.
39
+
40
+ - **TypeScript** – Strict types so you don’t ship dumb bugs.
41
+
42
+ - **TanStack Query** – Server state that doesn’t implode.
43
+
44
+ - **Lucide React** – Icons that don’t look like 2009 garbage.
45
+
46
+ - **Docker + Terraform** – Ship this shit properly.
47
+
48
+ - Terraform setup for Google Cloud Platform so you don’t YOLO infra.
49
+
50
+ ---
51
+
52
+ ## 🚀 Quick Start (Let’s fucking go)
53
+
54
+ ```bash
55
+ bunx create-ej-app@latest
5
56
  ```
57
+
58
+ You’ll be prompted for:
59
+
60
+ - Project name
61
+ - Description
62
+ - Template
63
+ - Git init (because not using git in 2026 is wild)
64
+
65
+ Example:
66
+
67
+ ```bash
68
+ ? Enter the project name: badass-app
69
+ ? Enter a description: My savage Next.js monster
70
+ ? Select a template: base
71
+ ? Initialize a git repository? yes
72
+ ```
73
+
74
+ Boom. Done.
75
+
76
+ ---
77
+
78
+ ## 📦 What You Actually Get
79
+
80
+ ### Tech Stack (The real weapons)
81
+
82
+ - **Framework**: Next.js
83
+ - **Runtime**: Bun (fast as shit)
84
+ - **Backend**: Elysia APIs
85
+ - **Database**: PostgreSQL + Drizzle
86
+ - **Auth**: Better Auth
87
+ - **Emails**: React Email (transactional + auth templates ready)
88
+ - **Observability**: OpenTelemetry
89
+ - **Background Jobs**: Workflow
90
+ - **UI**: shadcn/ui
91
+ - **Styling**: TailwindCSS
92
+ - **State**: TanStack Query
93
+ - **Forms**: react-hook-form + Zod
94
+ - **Theme**: Dark mode out of the box
95
+
96
+ ---
97
+
98
+ ## 🛠 Setup & Development
99
+
100
+ ### Prerequisites
101
+
102
+ - Node 24+ or Bun
103
+ - PostgreSQL running
104
+ - OAuth creds if you want GitHub/Google login
105
+ - Email provider (Resend, SES, Postmark, etc.)
106
+
107
+ ### Install
108
+
109
+ ```bash
110
+ cd my-app
111
+ bun install
112
+ # or npm / pnpm / yarn if that’s your thing
113
+ ```
114
+
115
+ ---
116
+
117
+ ### Environment Variables
118
+
119
+ Copy `.env.example` → `.env` and fill your secrets like a responsible adult.
120
+
121
+ Don’t commit this crap. Seriously.
122
+
123
+ ---
124
+
125
+ ## 💻 Development
126
+
127
+ ```bash
128
+ bun run dev
129
+ ```
130
+
131
+ Open `http://localhost:3000` and admire your badass setup.
132
+
133
+ ---
134
+
135
+ ## 🐳 Docker (Ship it like a pro)
136
+
137
+ ```bash
138
+ docker build -t my-app .
139
+ docker run -p 3000:3000 my-app
140
+ ```
141
+
142
+ No “works on my machine” bullshit anymore.
143
+
144
+ ---
145
+
146
+ ## ☁️ Infrastructure (The Cloud Shit)
147
+
148
+ Terraform config included for:
149
+
150
+ - Cloud Run
151
+ - Cloud SQL
152
+ - Artifact Registry
153
+ - Cloud Build
154
+ - Secret Manager
155
+ - Load Balancer
156
+ - VPC
157
+
158
+ Deploy it:
159
+
160
+ ```bash
161
+ cd terraform
162
+ terraform init
163
+ terraform apply
164
+ ```
165
+
166
+ And now you’re running production infra like a grown-ass engineer.
167
+
168
+ ---
169
+
170
+ ## 🔐 Authentication
171
+
172
+ Better Auth gives you:
173
+
174
+ - Email/Password login
175
+ - Magic links (no password drama)
176
+ - OAuth (GitHub, Google)
177
+ - Admin panel
178
+ - User impersonation
179
+
180
+ Secure, extensible, no duct-taped auth spaghetti.
181
+
182
+ ---
183
+
184
+ ## 🎨 UI Components
185
+
186
+ Preinstalled:
187
+
188
+ - Button
189
+ - Card
190
+ - Form
191
+ - Input
192
+ - Toast notifications
193
+
194
+ Need more?
195
+
196
+ ```bash
197
+ bunx shadcn@latest add [component-name]
198
+ ```
199
+
200
+ Ship beautiful shit without reinventing buttons for the 900th time.
201
+
202
+ ---
203
+
204
+ ## 🎯 Route Groups
205
+
206
+ Organized like a sane human:
207
+
208
+ - `(admin)` – Admin dashboard
209
+ - `(app)` – Main app (auth required)
210
+ - `(auth)` – Login/signup
211
+ - `(marketing)` – Public pages
212
+
213
+ No messy folder hell.
214
+
215
+ ---
216
+
217
+ ## 🔧 Customization
218
+
219
+ Edit:
220
+
221
+ - `siteConfig.ts` – change name/description
222
+ - `schema.ts` – change DB schema
223
+ - `auth.ts` – tweak providers
224
+
225
+ Then push schema and move on with your life.
226
+
227
+ ---
228
+
229
+ ## 🤝 Contributing
230
+
231
+ PRs welcome. Don’t submit half-broken shit.
232
+
233
+ ---
234
+
235
+ ## 📄 License
236
+
237
+ MIT. Do whatever the hell you want, just don’t blame us if you screw it up.
238
+
239
+ ---
240
+
241
+ ## 🔗 Links
242
+
243
+ - Repository: [https://github.com/ejekanshjain/create-ej-app](https://github.com/ejekanshjain/create-ej-app)
244
+ - Issues: [https://github.com/ejekanshjain/create-ej-app/issues](https://github.com/ejekanshjain/create-ej-app/issues)
245
+ - Docs:
246
+ - [Next.js](https://nextjs.org/docs)
247
+ - [Better Auth](https://better-auth.com)
248
+ - [Drizzle ORM](https://orm.drizzle.team)
249
+ - [Elysia](https://elysiajs.com)
250
+ - [Workflow](https://useworkflow.dev)
251
+ - [shadcn/ui](https://ui.shadcn.com)
252
+ - [React Email](https://react.email)
253
+ - [Logging](https://loggingsucks.com)
254
+
255
+ ---
256
+
257
+ Now go build some badass production-ready shit instead of another half-baked side project. 🚀
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ej-app",
3
- "version": "15.0.0",
3
+ "version": "17.0.0",
4
4
  "description": "Bootstrap a next.js app with my preferred setup",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -5,6 +5,9 @@
5
5
  "private": true,
6
6
  "scripts": {
7
7
  "dev": "next dev",
8
+ "email:dev": "email dev --dir src/emails",
9
+ "email:test": "bun ./src/scripts/test-mail.ts",
10
+ "workflow:inspect": "workflow inspect runs --web",
8
11
  "build": "bun --bun next build",
9
12
  "build:docker": "docker build -t my-app .",
10
13
  "analyze": "ANALYZE=true bun --bun next build --webpack",
@@ -17,20 +20,27 @@
17
20
  "gen:auth": "bunx @better-auth/cli generate"
18
21
  },
19
22
  "dependencies": {
20
- "@ejekanshjain/simple-email-validator": "1.0.2",
23
+ "@ejekanshjain/simple-email-validator": "1.0.3",
24
+ "@elysiajs/eden": "1.4.8",
21
25
  "@hookform/resolvers": "5.2.2",
22
26
  "@next/third-parties": "16.1.6",
27
+ "@opentelemetry/api": "1.9.0",
28
+ "@opentelemetry/sdk-trace-base": "2.5.1",
23
29
  "@paralleldrive/cuid2": "3.3.0",
24
30
  "@radix-ui/react-label": "2.1.8",
25
31
  "@radix-ui/react-separator": "1.1.8",
26
32
  "@radix-ui/react-slot": "1.2.4",
33
+ "@react-email/components": "1.0.7",
34
+ "@react-email/render": "2.0.4",
27
35
  "@t3-oss/env-nextjs": "0.13.10",
28
36
  "@tanstack/react-query": "5.90.21",
37
+ "@vercel/otel": "2.1.1",
29
38
  "better-auth": "1.4.18",
30
39
  "class-variance-authority": "0.7.1",
31
40
  "clsx": "2.1.1",
32
41
  "drizzle-orm": "0.45.1",
33
- "lucide-react": "0.563.0",
42
+ "elysia": "1.4.25",
43
+ "lucide-react": "0.564.0",
34
44
  "next": "16.1.6",
35
45
  "next-safe-action": "8.0.11",
36
46
  "next-themes": "0.4.6",
@@ -41,16 +51,17 @@
41
51
  "react-dom": "19.2.4",
42
52
  "react-hook-form": "7.71.1",
43
53
  "sonner": "2.0.7",
44
- "tailwind-merge": "3.4.0",
54
+ "tailwind-merge": "3.4.1",
45
55
  "tw-animate-css": "1.4.0",
46
- "workflow": "4.1.0-beta.55",
56
+ "workflow": "4.1.0-beta.57",
47
57
  "zod": "4.3.6"
48
58
  },
49
59
  "devDependencies": {
50
60
  "@next/bundle-analyzer": "16.1.6",
61
+ "@react-email/preview-server": "5.2.8",
51
62
  "@tailwindcss/postcss": "4.1.18",
52
63
  "@types/bun": "1.3.9",
53
- "@types/nodemailer": "7.0.9",
64
+ "@types/nodemailer": "7.0.10",
54
65
  "@types/react": "19.2.14",
55
66
  "@types/react-dom": "19.2.3",
56
67
  "babel-plugin-react-compiler": "1.0.0",
@@ -60,7 +71,16 @@
60
71
  "prettier": "3.8.1",
61
72
  "prettier-plugin-organize-imports": "4.3.0",
62
73
  "prettier-plugin-tailwindcss": "0.7.2",
74
+ "react-email": "5.2.8",
63
75
  "tailwindcss": "4.1.18",
64
76
  "typescript": "5.9.3"
65
- }
77
+ },
78
+ "trustedDependencies": [
79
+ "@nestjs/core",
80
+ "@swc/core",
81
+ "cbor-extract",
82
+ "esbuild",
83
+ "sharp",
84
+ "unrs-resolver"
85
+ ]
66
86
  }
@@ -0,0 +1,3 @@
1
+ export default async function AdminPage() {
2
+ return <div>Hello Admin</div>
3
+ }
@@ -0,0 +1,25 @@
1
+ import { Elysia } from 'elysia'
2
+ import z from 'zod'
3
+
4
+ export const elysiaApi = new Elysia({ prefix: '/api' })
5
+ .get('/', 'Hello from Elysia')
6
+ .post(
7
+ '/',
8
+ ({ body }) => {
9
+ return `Hello, ${body.name}!`
10
+ },
11
+ {
12
+ body: z.object({
13
+ name: z.string().trim().min(1).describe('Name of the user')
14
+ }),
15
+ detail: {
16
+ description: 'Greet a user by name'
17
+ }
18
+ }
19
+ )
20
+
21
+ export const GET = elysiaApi.fetch
22
+ export const POST = elysiaApi.fetch
23
+ export const PUT = elysiaApi.fetch
24
+ export const PATCH = elysiaApi.fetch
25
+ export const DELETE = elysiaApi.fetch
@@ -1,7 +1,13 @@
1
+ 'use client'
2
+
3
+ import { Button } from '@/components/ui/button'
1
4
  import Link from 'next/link'
2
- import { Button } from '~/components/ui/button'
5
+ import { usePathname } from 'next/navigation'
3
6
 
4
7
  export default function NotFound() {
8
+ const pathname = usePathname()
9
+ const homeRoute = pathname?.startsWith('/admin') ? '/admin' : '/'
10
+
5
11
  return (
6
12
  <main className="bg-background text-foreground flex min-h-screen flex-col">
7
13
  <section className="flex min-h-[60vh] flex-col items-center justify-center px-4">
@@ -12,7 +18,7 @@ export default function NotFound() {
12
18
  The page you're looking for doesn't exist.
13
19
  </p>
14
20
  <Button asChild>
15
- <Link href="/">Return to Home</Link>
21
+ <Link href={homeRoute}>Return to Home</Link>
16
22
  </Button>
17
23
  </div>
18
24
  </section>
@@ -0,0 +1,63 @@
1
+ import {
2
+ Body,
3
+ Container,
4
+ Head,
5
+ Hr,
6
+ Html,
7
+ pixelBasedPreset,
8
+ Preview,
9
+ Section,
10
+ Tailwind,
11
+ Text
12
+ } from '@react-email/components'
13
+
14
+ interface EmailLayoutProps {
15
+ previewText: string
16
+ companyName: string
17
+ children: React.ReactNode
18
+ }
19
+
20
+ export default function EmailLayout({
21
+ previewText,
22
+ companyName,
23
+ children
24
+ }: EmailLayoutProps) {
25
+ return (
26
+ <Html>
27
+ <Head />
28
+ <Preview>{previewText}</Preview>
29
+ <Tailwind
30
+ config={{
31
+ presets: [pixelBasedPreset]
32
+ }}
33
+ >
34
+ <Body className="bg-gray-100 font-sans">
35
+ <Container className="mx-auto my-8 max-w-xl rounded-lg bg-white shadow-sm">
36
+ {/* Header */}
37
+ <Section className="rounded-t-lg bg-black px-8 py-6">
38
+ <Text className="m-0 text-center text-2xl font-bold text-white">
39
+ {companyName}
40
+ </Text>
41
+ </Section>
42
+
43
+ {/* Content */}
44
+ <Section className="px-8 py-6">{children}</Section>
45
+
46
+ {/* Footer */}
47
+ <Section className="rounded-b-lg bg-gray-50 px-8 py-6">
48
+ <Hr className="my-4 border-gray-200" />
49
+ <Text className="m-0 text-center text-sm text-gray-500">
50
+ &copy; {new Date().getUTCFullYear()} {companyName}. All rights
51
+ reserved.
52
+ </Text>
53
+ <Text className="m-0 mt-2 text-center text-xs text-gray-400">
54
+ This is an automated email. Please do not reply directly to this
55
+ message.
56
+ </Text>
57
+ </Section>
58
+ </Container>
59
+ </Body>
60
+ </Tailwind>
61
+ </Html>
62
+ )
63
+ }
@@ -0,0 +1,49 @@
1
+ import { Button, Heading, Text } from '@react-email/components'
2
+ import EmailLayout from './components/email-layout'
3
+
4
+ export interface MagicLinkEmailProps {
5
+ magicLink: string
6
+ companyName: string
7
+ }
8
+
9
+ export default function MagicLinkEmail({
10
+ magicLink,
11
+ companyName
12
+ }: MagicLinkEmailProps) {
13
+ const previewText = `Your magic link to sign in to ${companyName}`
14
+
15
+ return (
16
+ <EmailLayout previewText={previewText} companyName={companyName}>
17
+ <Heading className="mb-4 text-2xl font-bold text-gray-900">
18
+ Sign in to {companyName}
19
+ </Heading>
20
+
21
+ <Text className="mb-4 text-base text-gray-700">
22
+ Click the button below to securely sign in to your account. This link
23
+ will expire in 5 minutes.
24
+ </Text>
25
+
26
+ <Button
27
+ href={magicLink}
28
+ className="mb-4 rounded-lg bg-black px-6 py-3 text-center text-base font-semibold text-white no-underline"
29
+ >
30
+ Sign In
31
+ </Button>
32
+
33
+ <Text className="mb-4 text-sm text-gray-600">
34
+ Or copy and paste this link into your browser:
35
+ </Text>
36
+
37
+ <Text className="mb-4 text-sm break-all text-blue-600">{magicLink}</Text>
38
+
39
+ <Text className="text-sm text-gray-500">
40
+ If you didn't request this email, you can safely ignore it.
41
+ </Text>
42
+ </EmailLayout>
43
+ )
44
+ }
45
+
46
+ MagicLinkEmail.PreviewProps = {
47
+ magicLink: 'https://example.com/magic-link?token=abc123',
48
+ companyName: 'Example Site'
49
+ } satisfies MagicLinkEmailProps
@@ -0,0 +1,32 @@
1
+ import { Button, Heading, Text } from '@react-email/components'
2
+ import EmailLayout from './components/email-layout'
3
+
4
+ interface TestEmailProps {
5
+ name: string
6
+ }
7
+
8
+ export default function TestEmail({ name }: TestEmailProps) {
9
+ return (
10
+ <EmailLayout previewText="This is a test email" companyName="Test">
11
+ <Heading className="mb-4 text-2xl font-bold text-gray-900">
12
+ Test Email for {name}
13
+ </Heading>
14
+ <Text className="mb-4 text-base text-gray-700">
15
+ This is a test email template to verify email functionality.
16
+ </Text>
17
+ <Text className="mb-6 text-base text-gray-700">
18
+ If you're seeing this, the email system is working correctly.
19
+ </Text>
20
+ <Button
21
+ href="https://example.com"
22
+ className="rounded-md bg-black px-5 py-3 text-center text-sm font-medium text-white"
23
+ >
24
+ Test Button
25
+ </Button>
26
+ </EmailLayout>
27
+ )
28
+ }
29
+
30
+ TestEmail.PreviewProps = {
31
+ name: 'John Doe'
32
+ }
@@ -0,0 +1,34 @@
1
+ import { Heading, Text } from '@react-email/components'
2
+ import EmailLayout from './components/email-layout'
3
+
4
+ export interface WelcomeEmailProps {
5
+ userName: string
6
+ companyName: string
7
+ }
8
+
9
+ export default function WelcomeEmail({
10
+ userName,
11
+ companyName
12
+ }: WelcomeEmailProps) {
13
+ const previewText = `Welcome to ${companyName}! Your account has been created successfully.`
14
+
15
+ return (
16
+ <EmailLayout previewText={previewText} companyName={companyName}>
17
+ <Heading className="mb-4 text-2xl font-bold text-gray-900">
18
+ Welcome to {companyName}! 🎉
19
+ </Heading>
20
+
21
+ <Text className="mb-4 text-base text-gray-700">Hi {userName},</Text>
22
+
23
+ <Text className="mb-4 text-base text-gray-700">
24
+ Thank you for creating an account with us. We're thrilled to have you as
25
+ part of our community!
26
+ </Text>
27
+ </EmailLayout>
28
+ )
29
+ }
30
+
31
+ WelcomeEmail.PreviewProps = {
32
+ userName: 'John Doe',
33
+ companyName: 'Example Site'
34
+ } satisfies WelcomeEmailProps
@@ -0,0 +1,26 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api'
2
+ import {
3
+ ConsoleSpanExporter,
4
+ ReadableSpan,
5
+ SimpleSpanProcessor
6
+ } from '@opentelemetry/sdk-trace-base'
7
+ import { registerOTel } from '@vercel/otel'
8
+
9
+ class SmartSamplingSpanProcessor extends SimpleSpanProcessor {
10
+ onEnd(span: ReadableSpan): void {
11
+ const isError = span.status.code === SpanStatusCode.ERROR
12
+ const isSlow = span.duration[0] * 1000 + span.duration[1] / 1e6 > 5000
13
+ const isLucky = Math.random() < 0 // Adjust this value to adjust the sampling rate for non-error and non-slow spans
14
+
15
+ if (isError || isSlow || isLucky) {
16
+ super.onEnd(span)
17
+ }
18
+ }
19
+ }
20
+
21
+ export function register() {
22
+ registerOTel({
23
+ serviceName: 'tdc-ai',
24
+ spanProcessors: [new SmartSamplingSpanProcessor(new ConsoleSpanExporter())]
25
+ })
26
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from 'better-auth/plugins'
11
11
  import { headers } from 'next/headers'
12
12
  import { cache } from 'react'
13
+ import { start } from 'workflow/api'
13
14
  import { db } from '~/db'
14
15
  import {
15
16
  accountsTable,
@@ -18,6 +19,8 @@ import {
18
19
  verificationsTable
19
20
  } from '~/db/schema'
20
21
  import { env } from '~/env'
22
+ import { sendMagicLinkEmail, sendWelcomeEmail } from './email-service'
23
+ import { siteConfig } from './siteConfig'
21
24
 
22
25
  export const auth = betterAuth({
23
26
  database: drizzleAdapter(db, {
@@ -51,14 +54,39 @@ export const auth = betterAuth({
51
54
  clientSecret: env.BETTER_AUTH_GOOGLE_SECRET
52
55
  }
53
56
  },
57
+ databaseHooks: {
58
+ user: {
59
+ create: {
60
+ after: async user => {
61
+ try {
62
+ await start(sendWelcomeEmail, [
63
+ user.email,
64
+ {
65
+ userName: user.name,
66
+ companyName: siteConfig.name
67
+ }
68
+ ])
69
+ } catch (error) {
70
+ console.error('Failed to send welcome email:', error)
71
+ }
72
+ }
73
+ }
74
+ }
75
+ },
54
76
  plugins: [
55
77
  magicLink({
56
- sendMagicLink: async ({ email, token, url }) => {
57
- console.warn('TODO: send email', {
58
- email,
59
- token,
60
- url
61
- })
78
+ sendMagicLink: async ({ email, url }) => {
79
+ try {
80
+ await start(sendMagicLinkEmail, [
81
+ email,
82
+ {
83
+ magicLink: url,
84
+ companyName: siteConfig.name
85
+ }
86
+ ])
87
+ } catch (error) {
88
+ console.error('Failed to send magic link email:', error)
89
+ }
62
90
  }
63
91
  }),
64
92
  admin(),
@@ -66,10 +94,7 @@ export const auth = betterAuth({
66
94
  ],
67
95
  hooks: {
68
96
  before: createAuthMiddleware(async ctx => {
69
- if (
70
- ctx.path === '/sign-in/magic-link' &&
71
- ctx.body?.email
72
- ) {
97
+ if (ctx.path === '/sign-in/magic-link' && ctx.body?.email) {
73
98
  try {
74
99
  const validation = await validateEmail({
75
100
  email: ctx.body.email
@@ -0,0 +1,7 @@
1
+ import { treaty } from '@elysiajs/eden'
2
+ import { elysiaApi } from '~/app/api/[[...slugs]]/route'
3
+
4
+ export const api =
5
+ typeof process !== 'undefined'
6
+ ? treaty(elysiaApi).api
7
+ : treaty<typeof elysiaApi>(window.location.host).api
@@ -0,0 +1,70 @@
1
+ import { render, toPlainText } from '@react-email/render'
2
+ import MagicLinkEmail, { MagicLinkEmailProps } from '~/emails/magic-link'
3
+ import WelcomeEmail, { WelcomeEmailProps } from '~/emails/welcome'
4
+ import { sendEmail } from './nodemailer'
5
+
6
+ /**
7
+ * Mapping of template types to their corresponding props
8
+ */
9
+ type EmailTemplateMap = {
10
+ welcome: WelcomeEmailProps
11
+ magicLink: MagicLinkEmailProps
12
+ }
13
+
14
+ type EmailTemplateType = keyof EmailTemplateMap
15
+
16
+ // Map of email template components with proper type safety
17
+ const componentMap: Record<
18
+ EmailTemplateType,
19
+ (props: EmailTemplateMap[EmailTemplateType]) => React.ReactElement
20
+ > = {
21
+ welcome: props => WelcomeEmail(props as WelcomeEmailProps),
22
+ magicLink: props => MagicLinkEmail(props as MagicLinkEmailProps)
23
+ }
24
+
25
+ async function renderEmailTemplate<T extends EmailTemplateType>(
26
+ templateType: T,
27
+ props: EmailTemplateMap[T]
28
+ ): Promise<{ html: string; text: string }>
29
+
30
+ async function renderEmailTemplate(
31
+ templateType: EmailTemplateType,
32
+ props: EmailTemplateMap[EmailTemplateType]
33
+ ) {
34
+ 'use step'
35
+
36
+ const emailComponent = componentMap[templateType](props)
37
+
38
+ const html = await render(emailComponent)
39
+ const text = toPlainText(html)
40
+ return { html, text }
41
+ }
42
+
43
+ export async function sendWelcomeEmail(to: string, props: WelcomeEmailProps) {
44
+ 'use workflow'
45
+
46
+ const { html, text } = await renderEmailTemplate('welcome', props)
47
+
48
+ return await sendEmail({
49
+ to,
50
+ subject: `Welcome to ${props.companyName}! 🎉`,
51
+ html,
52
+ text
53
+ })
54
+ }
55
+
56
+ export async function sendMagicLinkEmail(
57
+ to: string,
58
+ props: MagicLinkEmailProps
59
+ ) {
60
+ 'use workflow'
61
+
62
+ const { html, text } = await renderEmailTemplate('magicLink', props)
63
+
64
+ return await sendEmail({
65
+ to,
66
+ subject: `Sign in to ${props.companyName}`,
67
+ html,
68
+ text
69
+ })
70
+ }
@@ -0,0 +1,43 @@
1
+ import nodemailer, { SentMessageInfo } from 'nodemailer'
2
+ import { env } from '~/env'
3
+
4
+ const transporter = nodemailer.createTransport({
5
+ host: env.EMAIL_SERVER_HOST,
6
+ port: parseInt(env.EMAIL_SERVER_PORT),
7
+ secure: parseInt(env.EMAIL_SERVER_PORT) === 465, // Use secure for port 465, false for other ports
8
+ auth: {
9
+ user: env.EMAIL_SERVER_USER,
10
+ pass: env.EMAIL_SERVER_PASSWORD
11
+ }
12
+ })
13
+
14
+ interface SendEmailParams {
15
+ to: string
16
+ subject: string
17
+ text?: string
18
+ html?: string
19
+ }
20
+
21
+ export const sendEmail = async ({
22
+ to,
23
+ subject,
24
+ text,
25
+ html
26
+ }: SendEmailParams): Promise<SentMessageInfo> => {
27
+ 'use step'
28
+
29
+ try {
30
+ const info = await transporter.sendMail({
31
+ from: env.EMAIL_FROM,
32
+ to,
33
+ subject,
34
+ text,
35
+ html
36
+ })
37
+
38
+ return info
39
+ } catch (error) {
40
+ console.error('Error sending email:', error)
41
+ throw new Error('Failed to send email')
42
+ }
43
+ }
@@ -0,0 +1,27 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api'
2
+
3
+ function getSpan() {
4
+ const span = trace.getActiveSpan()
5
+ return span
6
+ }
7
+
8
+ export function recordWideEvent(attributes: Record<string, any>) {
9
+ const span = getSpan()
10
+ if (!span) return
11
+
12
+ span.setAttributes(attributes)
13
+ }
14
+
15
+ export function recordError(err: unknown) {
16
+ const span = getSpan()
17
+ if (!span) return
18
+
19
+ span.setStatus({
20
+ code: SpanStatusCode.ERROR,
21
+ message: err instanceof Error ? err.message : String(err)
22
+ })
23
+
24
+ span.recordException(err instanceof Error ? err : String(err))
25
+
26
+ return span.spanContext().traceId
27
+ }
@@ -1,10 +1,18 @@
1
1
  import { createSafeActionClient } from 'next-safe-action'
2
2
  import { getAuthSession } from './auth'
3
+ import { recordWideEvent } from './otel'
3
4
 
4
5
  export const actionClient = createSafeActionClient().use(
5
6
  async ({ next, ctx }) => {
6
7
  const authSession = await getAuthSession()
7
8
 
9
+ if (authSession)
10
+ recordWideEvent({
11
+ 'user.id': authSession.user.id,
12
+ 'user.admin': authSession.isAdmin,
13
+ 'session.id': authSession.session.id
14
+ })
15
+
8
16
  return next({
9
17
  ctx: {
10
18
  ...ctx,
@@ -0,0 +1,29 @@
1
+ import { render, toPlainText } from '@react-email/render'
2
+ import TestEmail from '~/emails/test'
3
+ import { sendEmail } from '~/lib/nodemailer'
4
+
5
+ async function main() {
6
+ const html = await render(
7
+ TestEmail({
8
+ name: 'Ekansh Jain'
9
+ }),
10
+ {
11
+ pretty: true
12
+ }
13
+ )
14
+
15
+ const text = toPlainText(html)
16
+
17
+ const result = await sendEmail({
18
+ to: 'john@example.com',
19
+ subject: 'Test Email',
20
+ text,
21
+ html
22
+ })
23
+
24
+ console.info('Test email sent successfully', result)
25
+
26
+ process.exit()
27
+ }
28
+
29
+ main()
@@ -33,7 +33,7 @@ resource "google_cloud_run_v2_service" "my_app_prod" {
33
33
  cpu = "1000m"
34
34
  memory = "1Gi"
35
35
  }
36
- cpu_idle = true
36
+ cpu_idle = true # Set to false when using workflow
37
37
  startup_cpu_boost = true
38
38
  }
39
39