saas-init 1.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/README.md +98 -0
- package/dist/index.js +765 -0
- package/package.json +68 -0
- package/templates/auth/clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/auth/clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/auth/clerk/middleware.ts +12 -0
- package/templates/auth/nextauth/app/api/auth/route.ts +3 -0
- package/templates/auth/nextauth/auth.ts +12 -0
- package/templates/auth/supabase/middleware.ts +59 -0
- package/templates/auth/supabase/utils/supabase/client.ts +12 -0
- package/templates/auth/supabase/utils/supabase/server.ts +35 -0
- package/templates/base/app/globals.css +87 -0
- package/templates/base/app/layout.tsx +19 -0
- package/templates/base/app/page.tsx +7 -0
- package/templates/base/components.json +21 -0
- package/templates/base/lib/utils.ts +6 -0
- package/templates/base/next.config.ts +7 -0
- package/templates/base/package.json +23 -0
- package/templates/base/postcss.config.mjs +5 -0
- package/templates/base/tsconfig.json +27 -0
- package/templates/database/postgres/db/index.ts +12 -0
- package/templates/database/postgres/db/schema.ts +7 -0
- package/templates/database/postgres/drizzle.config.ts +10 -0
- package/templates/database/sqlite/db/index.ts +7 -0
- package/templates/database/sqlite/db/schema.ts +7 -0
- package/templates/database/sqlite/drizzle.config.ts +10 -0
- package/templates/database/supabase/utils/supabase/client.ts +12 -0
- package/templates/database/supabase/utils/supabase/db.ts +10 -0
- package/templates/docker/.dockerignore +5 -0
- package/templates/docker/Dockerfile +25 -0
- package/templates/docker/docker-compose.yml +22 -0
- package/templates/email/postmark/lib/email.ts +17 -0
- package/templates/email/resend/lib/email.ts +20 -0
- package/templates/github/.github/workflows/ci.yml +55 -0
- package/templates/landing/app/page.tsx +21 -0
- package/templates/landing/components/Footer.tsx +37 -0
- package/templates/landing/components/Hero.tsx +29 -0
- package/templates/landing/components/ProblemAgitate.tsx +34 -0
- package/templates/landing/components/SecondaryCTA.tsx +20 -0
- package/templates/landing/components/SocialProof.tsx +43 -0
- package/templates/landing/components/Transformation.tsx +48 -0
- package/templates/landing/components/ValueStack.tsx +54 -0
- package/templates/payments/lemonsqueezy/app/api/webhooks/lemonsqueezy/route.ts +58 -0
- package/templates/payments/lemonsqueezy/lib/lemonsqueezy.ts +13 -0
- package/templates/payments/stripe/app/api/webhooks/stripe/route.ts +46 -0
- package/templates/payments/stripe/lib/stripe.ts +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saas-init",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI scaffolding tool that generates production-ready SaaS projects with Next.js, auth, payments, database, and email — configured and ready to ship.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"saas-init": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@clack/prompts": "^0.9.1",
|
|
16
|
+
"commander": "^12.1.0",
|
|
17
|
+
"fs-extra": "^11.2.0",
|
|
18
|
+
"zod": "^3.23.8"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/fs-extra": "^11.0.4",
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
24
|
+
"eslint": "^9.0.0",
|
|
25
|
+
"prettier": "^3.3.3",
|
|
26
|
+
"tsup": "^8.3.0",
|
|
27
|
+
"typescript": "^5.6.3",
|
|
28
|
+
"vitest": "^2.1.8"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"saas",
|
|
32
|
+
"scaffold",
|
|
33
|
+
"cli",
|
|
34
|
+
"nextjs",
|
|
35
|
+
"starter",
|
|
36
|
+
"boilerplate",
|
|
37
|
+
"clerk",
|
|
38
|
+
"nextauth",
|
|
39
|
+
"supabase",
|
|
40
|
+
"stripe",
|
|
41
|
+
"drizzle",
|
|
42
|
+
"tailwind",
|
|
43
|
+
"shadcn"
|
|
44
|
+
],
|
|
45
|
+
"author": "Oleg Koval",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/olegkoval/saas-init.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/olegkoval/saas-init#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/olegkoval/saas-init/issues"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"dev": "tsup --watch",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest",
|
|
63
|
+
"lint": "eslint src tests",
|
|
64
|
+
"format": "prettier --write .",
|
|
65
|
+
"format:check": "prettier --check .",
|
|
66
|
+
"typecheck": "tsc --noEmit"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clerkMiddleware } from '@clerk/nextjs/server'
|
|
2
|
+
|
|
3
|
+
export default clerkMiddleware()
|
|
4
|
+
|
|
5
|
+
export const config = {
|
|
6
|
+
matcher: [
|
|
7
|
+
// Skip Next.js internals and all static files, unless found in search params
|
|
8
|
+
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
|
9
|
+
// Always run for API routes
|
|
10
|
+
'/(api|trpc)(.*)',
|
|
11
|
+
],
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import NextAuth from 'next-auth'
|
|
2
|
+
|
|
3
|
+
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
4
|
+
providers: [
|
|
5
|
+
// Add your providers here
|
|
6
|
+
// Example:
|
|
7
|
+
// GitHub({
|
|
8
|
+
// clientId: process.env.GITHUB_ID!,
|
|
9
|
+
// clientSecret: process.env.GITHUB_SECRET!,
|
|
10
|
+
// }),
|
|
11
|
+
],
|
|
12
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createServerClient } from '@supabase/ssr'
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
3
|
+
|
|
4
|
+
export async function updateSession(request: NextRequest) {
|
|
5
|
+
let supabaseResponse = NextResponse.next({
|
|
6
|
+
request,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
10
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
11
|
+
|
|
12
|
+
if (!url || !anonKey) {
|
|
13
|
+
throw new Error('Missing required Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const supabase = createServerClient(
|
|
17
|
+
url,
|
|
18
|
+
anonKey,
|
|
19
|
+
{
|
|
20
|
+
cookies: {
|
|
21
|
+
getAll() {
|
|
22
|
+
return request.cookies.getAll()
|
|
23
|
+
},
|
|
24
|
+
setAll(cookiesToSet) {
|
|
25
|
+
cookiesToSet.forEach(({ name, value }) =>
|
|
26
|
+
request.cookies.set(name, value)
|
|
27
|
+
)
|
|
28
|
+
supabaseResponse = NextResponse.next({
|
|
29
|
+
request,
|
|
30
|
+
})
|
|
31
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
32
|
+
supabaseResponse.cookies.set(name, value, options)
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
await supabase.auth.getUser()
|
|
40
|
+
|
|
41
|
+
return supabaseResponse
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default async function middleware(request: NextRequest) {
|
|
45
|
+
try {
|
|
46
|
+
return await updateSession(request)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Return 500 error response instead of throwing to avoid middleware crash
|
|
49
|
+
return new NextResponse('Internal Server Error: Missing Supabase configuration', {
|
|
50
|
+
status: 500,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const config = {
|
|
56
|
+
matcher: [
|
|
57
|
+
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
58
|
+
],
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
2
|
+
|
|
3
|
+
export function createClient() {
|
|
4
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
5
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
6
|
+
|
|
7
|
+
if (!url || !anonKey) {
|
|
8
|
+
throw new Error('Missing required Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return createBrowserClient(url, anonKey)
|
|
12
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createServerClient } from '@supabase/ssr'
|
|
2
|
+
import { cookies } from 'next/headers'
|
|
3
|
+
|
|
4
|
+
export async function createClient() {
|
|
5
|
+
const cookieStore = await cookies()
|
|
6
|
+
|
|
7
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
8
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
9
|
+
|
|
10
|
+
if (!url || !anonKey) {
|
|
11
|
+
throw new Error('Missing required Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return createServerClient(
|
|
15
|
+
url,
|
|
16
|
+
anonKey,
|
|
17
|
+
{
|
|
18
|
+
cookies: {
|
|
19
|
+
getAll() {
|
|
20
|
+
return cookieStore.getAll()
|
|
21
|
+
},
|
|
22
|
+
setAll(cookiesToSet) {
|
|
23
|
+
try {
|
|
24
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
25
|
+
cookieStore.set(name, value, options)
|
|
26
|
+
)
|
|
27
|
+
} catch {
|
|
28
|
+
// setAll called from a Server Component — can be ignored if
|
|
29
|
+
// session refresh middleware is in place
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme inline {
|
|
4
|
+
--color-background: var(--background);
|
|
5
|
+
--color-foreground: var(--foreground);
|
|
6
|
+
--color-card: var(--card);
|
|
7
|
+
--color-card-foreground: var(--card-foreground);
|
|
8
|
+
--color-popover: var(--popover);
|
|
9
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
10
|
+
--color-primary: var(--primary);
|
|
11
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
12
|
+
--color-secondary: var(--secondary);
|
|
13
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
14
|
+
--color-muted: var(--muted);
|
|
15
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
16
|
+
--color-accent: var(--accent);
|
|
17
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
18
|
+
--color-destructive: var(--destructive);
|
|
19
|
+
--color-border: var(--border);
|
|
20
|
+
--color-input: var(--input);
|
|
21
|
+
--color-ring: var(--ring);
|
|
22
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
23
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
24
|
+
--radius-lg: var(--radius);
|
|
25
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:root {
|
|
29
|
+
--background: oklch(1 0 0);
|
|
30
|
+
--foreground: oklch(0.145 0 0);
|
|
31
|
+
--card: oklch(1 0 0);
|
|
32
|
+
--card-foreground: oklch(0.145 0 0);
|
|
33
|
+
--popover: oklch(1 0 0);
|
|
34
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
35
|
+
--primary: oklch(0.205 0 0);
|
|
36
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
37
|
+
--secondary: oklch(0.97 0 0);
|
|
38
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
39
|
+
--muted: oklch(0.97 0 0);
|
|
40
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
41
|
+
--accent: oklch(0.97 0 0);
|
|
42
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
43
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
44
|
+
--border: oklch(0.922 0 0);
|
|
45
|
+
--input: oklch(0.922 0 0);
|
|
46
|
+
--ring: oklch(0.708 0 0);
|
|
47
|
+
--radius: 0.625rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dark {
|
|
51
|
+
--background: oklch(0.145 0 0);
|
|
52
|
+
--foreground: oklch(0.985 0 0);
|
|
53
|
+
--card: oklch(0.205 0 0);
|
|
54
|
+
--card-foreground: oklch(0.985 0 0);
|
|
55
|
+
--popover: oklch(0.205 0 0);
|
|
56
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
57
|
+
--primary: oklch(0.922 0 0);
|
|
58
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
59
|
+
--secondary: oklch(0.269 0 0);
|
|
60
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
61
|
+
--muted: oklch(0.269 0 0);
|
|
62
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
63
|
+
--accent: oklch(0.269 0 0);
|
|
64
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
65
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
66
|
+
--border: oklch(1 0 0 / 10%);
|
|
67
|
+
--input: oklch(1 0 0 / 15%);
|
|
68
|
+
--ring: oklch(0.556 0 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
* {
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
padding: 0;
|
|
74
|
+
margin: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
html,
|
|
78
|
+
body {
|
|
79
|
+
max-width: 100vw;
|
|
80
|
+
overflow-x: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
body {
|
|
84
|
+
background: var(--background);
|
|
85
|
+
color: var(--foreground);
|
|
86
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
87
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import './globals.css'
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: '{{name}}',
|
|
6
|
+
description: 'Generated by saas-init',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body>{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
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
|
+
"test": "echo \"No tests configured\" && exit 0"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "^15.0.0",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"@types/react": "^19.0.0",
|
|
20
|
+
"@types/react-dom": "^19.0.0",
|
|
21
|
+
"typescript": "^5.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
2
|
+
import postgres from 'postgres'
|
|
3
|
+
import * as schema from './schema'
|
|
4
|
+
|
|
5
|
+
const databaseUrl = process.env.DATABASE_URL
|
|
6
|
+
if (!databaseUrl) {
|
|
7
|
+
throw new Error('Missing required environment variable: DATABASE_URL')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const client = postgres(databaseUrl)
|
|
11
|
+
|
|
12
|
+
export const db = drizzle(client, { schema })
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
export const users = sqliteTable('users', {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
email: text('email').notNull().unique(),
|
|
6
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()).notNull(),
|
|
7
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js'
|
|
2
|
+
|
|
3
|
+
export function createBrowserClient() {
|
|
4
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
5
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
6
|
+
|
|
7
|
+
if (!url || !anonKey) {
|
|
8
|
+
throw new Error('Missing required Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return createClient(url, anonKey)
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js'
|
|
2
|
+
|
|
3
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
4
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
5
|
+
|
|
6
|
+
if (!url || !anonKey) {
|
|
7
|
+
throw new Error('Missing required Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const supabase = createClient(url, anonKey)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Stage 1: Install dependencies
|
|
2
|
+
FROM node:20-alpine AS deps
|
|
3
|
+
RUN corepack enable
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY package.json pnpm-lock.yaml ./
|
|
6
|
+
RUN pnpm install --frozen-lockfile
|
|
7
|
+
|
|
8
|
+
# Stage 2: Build the application
|
|
9
|
+
FROM node:20-alpine AS builder
|
|
10
|
+
RUN corepack enable
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
13
|
+
COPY . .
|
|
14
|
+
RUN pnpm build
|
|
15
|
+
|
|
16
|
+
# Stage 3: Production runner
|
|
17
|
+
FROM node:20-alpine AS runner
|
|
18
|
+
LABEL app="{{name}}"
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
ENV NODE_ENV=production
|
|
21
|
+
COPY --from=builder /app/public ./public
|
|
22
|
+
COPY --from=builder /app/.next/standalone ./
|
|
23
|
+
COPY --from=builder /app/.next/static ./.next/static
|
|
24
|
+
EXPOSE 3000
|
|
25
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
services:
|
|
2
|
+
{{name}}:
|
|
3
|
+
build: .
|
|
4
|
+
ports:
|
|
5
|
+
- "3000:3000"
|
|
6
|
+
env_file:
|
|
7
|
+
- .env.local
|
|
8
|
+
|
|
9
|
+
# Uncomment to add a Postgres database:
|
|
10
|
+
# postgres:
|
|
11
|
+
# image: postgres:16-alpine
|
|
12
|
+
# environment:
|
|
13
|
+
# POSTGRES_USER: postgres
|
|
14
|
+
# POSTGRES_PASSWORD: password
|
|
15
|
+
# POSTGRES_DB: {{name}}
|
|
16
|
+
# ports:
|
|
17
|
+
# - "5432:5432"
|
|
18
|
+
# volumes:
|
|
19
|
+
# - postgres_data:/var/lib/postgresql/data
|
|
20
|
+
|
|
21
|
+
# volumes:
|
|
22
|
+
# postgres_data:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as postmark from 'postmark'
|
|
2
|
+
|
|
3
|
+
const apiToken = process.env.POSTMARK_API_TOKEN
|
|
4
|
+
if (!apiToken) {
|
|
5
|
+
throw new Error('Missing required environment variable: POSTMARK_API_TOKEN')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const client = new postmark.ServerClient(apiToken)
|
|
9
|
+
|
|
10
|
+
export async function sendEmail(to: string, subject: string, html: string): Promise<void> {
|
|
11
|
+
await client.sendEmail({
|
|
12
|
+
From: 'noreply@example.com',
|
|
13
|
+
To: to,
|
|
14
|
+
Subject: subject,
|
|
15
|
+
HtmlBody: html,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Resend } from 'resend'
|
|
2
|
+
|
|
3
|
+
const apiKey = process.env.RESEND_API_KEY
|
|
4
|
+
if (!apiKey) {
|
|
5
|
+
throw new Error('Missing required environment variable: RESEND_API_KEY')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const resend = new Resend(apiKey)
|
|
9
|
+
|
|
10
|
+
export async function sendEmail(to: string, subject: string, html: string): Promise<void> {
|
|
11
|
+
const { error } = await resend.emails.send({
|
|
12
|
+
from: 'noreply@example.com',
|
|
13
|
+
to,
|
|
14
|
+
subject,
|
|
15
|
+
html,
|
|
16
|
+
})
|
|
17
|
+
if (error) {
|
|
18
|
+
throw new Error(`Failed to send email: ${error.message}`)
|
|
19
|
+
}
|
|
20
|
+
}
|