├── sst-env.d.ts
├── .eslintrc.json
├── src
├── migrations
│ ├── 0004_greedy_speed_demon.sql
│ ├── 0002_light_celestials.sql
│ ├── 0003_silly_surge.sql
│ ├── 0001_large_mathemanic.sql
│ ├── 0000_eager_silver_sable.sql
│ └── meta
│ │ ├── _journal.json
│ │ ├── 0000_snapshot.json
│ │ ├── 0002_snapshot.json
│ │ ├── 0001_snapshot.json
│ │ ├── 0003_snapshot.json
│ │ └── 0004_snapshot.json
└── app
│ ├── favicon.ico
│ ├── page.js
│ ├── layout.js
│ ├── lib
│ ├── schema.js
│ ├── secrets.js
│ └── db.js
│ ├── api
│ ├── route.js
│ └── leads
│ │ └── route.js
│ ├── leadCapture
│ ├── hero.js
│ └── forms.js
│ ├── globals.css
│ └── cli
│ └── migrator.js
├── jsconfig.json
├── leads-landing.code-workspace
├── next.config.js
├── postcss.config.js
├── public
├── launching.jpg
├── vercel.svg
└── next.svg
├── drizzle.config.js
├── README.md
├── .gitignore
├── tailwind.config.js
├── sst.config.ts
├── package.json
└── .github
└── workflows
├── main.yaml
├── production.yaml
└── stage.yaml
/sst-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/src/migrations/0004_greedy_speed_demon.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "leads" ADD COLUMN "description" text;
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/leads-landing.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {}
8 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/deploy-nextjs-neon-sst-aws-tutorial/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/public/launching.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/deploy-nextjs-neon-sst-aws-tutorial/HEAD/public/launching.jpg
--------------------------------------------------------------------------------
/drizzle.config.js:
--------------------------------------------------------------------------------
1 | const drizzleConfig = {
2 | schema: './src/app/lib/schema.js',
3 | out: './src/migrations'
4 | }
5 |
6 | export default drizzleConfig;
--------------------------------------------------------------------------------
/src/app/page.js:
--------------------------------------------------------------------------------
1 |
2 | import HeroLandingPage from '@/app/leadCapture/hero'
3 |
4 |
5 | export default function Home() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/migrations/0002_light_celestials.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "leads" DROP COLUMN IF EXISTS "first_name";--> statement-breakpoint
2 | ALTER TABLE "leads" DROP COLUMN IF EXISTS "last_name";
--------------------------------------------------------------------------------
/src/migrations/0003_silly_surge.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "leads" ADD COLUMN "first_name" varchar(150);--> statement-breakpoint
2 | ALTER TABLE "leads" ADD COLUMN "last_name" varchar(150);
--------------------------------------------------------------------------------
/src/migrations/0001_large_mathemanic.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "leads" ADD COLUMN "first_name" varchar(150);--> statement-breakpoint
2 | ALTER TABLE "leads" ADD COLUMN "last_name" varchar(150);
--------------------------------------------------------------------------------
/src/migrations/0000_eager_silver_sable.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "leads" (
2 | "id" serial PRIMARY KEY NOT NULL,
3 | "email" text NOT NULL,
4 | "created_at" timestamp DEFAULT now()
5 | );
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deploy Next.js with Neon, SST, & AWS
2 |
3 | In this tutorial series, you'll learn how to deploy a Next.js project that is made to collect emails from leads and store them in a Neon Postgres database. The prjoect will be deployed on various AWS services, such as Lambda and Cloudfront, using the tool SST.
4 |
5 |
6 | ## Setup
--------------------------------------------------------------------------------
/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import { Inter } from 'next/font/google'
3 |
4 | const inter = Inter({ subsets: ['latin'] })
5 |
6 | export const metadata = {
7 | title: 'Sales Landing Page with Next, AWS & Neon',
8 | description: 'Sales Landing Page with Next + Neon',
9 | }
10 |
11 | export default function RootLayout({ children }) {
12 | return (
13 |
14 |
{children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .open-next/
3 | .sst
4 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
5 |
6 | # dependencies
7 | /node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # next.js
15 | /.next/
16 | /out/
17 |
18 | # production
19 | /build
20 |
21 | # misc
22 | .DS_Store
23 | *.pem
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | }
19 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sst.config.ts:
--------------------------------------------------------------------------------
1 | import { SSTConfig } from "sst";
2 | import { Config, NextjsSite } from "sst/constructs";
3 |
4 | export default {
5 | config(_input) {
6 | return {
7 | name: "leads-landing",
8 | region: "us-east-1",
9 | };
10 | },
11 | stacks(app) {
12 | app.stack(function Site({ stack }) {
13 | const DATABASE_URL = new Config.Secret(stack, "DATABASE_URL")
14 | const SECRET_VAL = new Config.Secret(stack, "SECRET_VAL")
15 | const site = new NextjsSite(stack, "site", {
16 | bind: [DATABASE_URL, SECRET_VAL],
17 | });
18 |
19 |
20 | stack.addOutputs({
21 | SiteUrl: site.url,
22 | });
23 | });
24 | },
25 | } satisfies SSTConfig;
26 |
--------------------------------------------------------------------------------
/src/app/lib/schema.js:
--------------------------------------------------------------------------------
1 | import validator from 'validator'
2 | import {serial, pgTable, text, timestamp, varchar} from 'drizzle-orm/pg-core'
3 | import { createInsertSchema } from 'drizzle-zod'
4 |
5 | export const LeadTable = pgTable("leads", {
6 | id: serial("id").primaryKey().notNull(),
7 | email: text("email").notNull(),
8 | firstName: varchar("first_name", {length: 150}),
9 | lastName: varchar("last_name", {length: 150}),
10 | description: text("description"),
11 | createdAt: timestamp("created_at").defaultNow(),
12 | })
13 |
14 | export const insertLeadTableSchema = createInsertSchema(LeadTable, {
15 | email: (schema) => schema.email.email(),
16 | firstName: (schema) => schema.firstName.min(2).max(150).optional(),
17 | lastName: (schema) => schema.lastName.min(2).max(150).optional(),
18 | })
--------------------------------------------------------------------------------
/src/app/lib/secrets.js:
--------------------------------------------------------------------------------
1 | import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
2 |
3 | const STAGE = process.env.STAGE ? process.env.STAGE : "production"
4 | const PROJECT = "leads-landing"
5 | const REGION = "us-east-1"
6 |
7 | export default async function getSecret (secretName) {
8 | if (!secretName) {
9 | return null
10 | }
11 | const client = new SSMClient({region: REGION})
12 | const paramName = `/sst/${PROJECT}/${STAGE}/Secret/${secretName}/value`
13 | const paramOptions = {
14 | Name: paramName,
15 | WithDecryption: true
16 | }
17 | const command = new GetParameterCommand(paramOptions)
18 | try {
19 | const response = await client.send(command)
20 | return response.Parameter.Value
21 | } catch (error) {
22 | return null
23 | }
24 | }
--------------------------------------------------------------------------------
/src/app/api/route.js:
--------------------------------------------------------------------------------
1 | import {NextResponse} from 'next/server'
2 | import {Config} from 'sst/node/config'
3 |
4 | import {dbNow, addLead} from '@/app/lib/db'
5 |
6 | export const dynamic = 'force-dynamic'
7 | export const runtime = 'nodejs'
8 | export const fetchCache = 'force-no-store'
9 | export const revalidate = 0
10 |
11 | export async function GET(request){
12 | const secretVal = Config.SECRET_VAL
13 | const dbString = Config.DATABASE_URL
14 | const stage = Config.STAGE
15 | const dbResult = await dbNow()
16 | const leadResult = await addLead({email: "abc123@abc123.com"})
17 | const now = dbResult ? dbResult[0].now : null
18 | return NextResponse.json({
19 | hello: "World",
20 | stage: stage,
21 | secretVal: secretVal,
22 | leadResult: leadResult,
23 | dbString: `${dbString}`.slice(0, 25),
24 | now: now
25 | }, {status: 200})
26 |
27 | }
--------------------------------------------------------------------------------
/src/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "5",
8 | "when": 1697151853305,
9 | "tag": "0000_eager_silver_sable",
10 | "breakpoints": true
11 | },
12 | {
13 | "idx": 1,
14 | "version": "5",
15 | "when": 1697151983893,
16 | "tag": "0001_large_mathemanic",
17 | "breakpoints": true
18 | },
19 | {
20 | "idx": 2,
21 | "version": "5",
22 | "when": 1697152015907,
23 | "tag": "0002_light_celestials",
24 | "breakpoints": true
25 | },
26 | {
27 | "idx": 3,
28 | "version": "5",
29 | "when": 1697152025585,
30 | "tag": "0003_silly_surge",
31 | "breakpoints": true
32 | },
33 | {
34 | "idx": 4,
35 | "version": "5",
36 | "when": 1697215146820,
37 | "tag": "0004_greedy_speed_demon",
38 | "breakpoints": true
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/src/app/lib/db.js:
--------------------------------------------------------------------------------
1 | import { neon, neonConfig } from '@neondatabase/serverless';
2 | import { drizzle } from 'drizzle-orm/neon-http';
3 | import { Config } from 'sst/node/config'
4 |
5 | import * as schema from '@/app/lib/schema'
6 |
7 | export async function dbClient (useSQLOnly) {
8 | neonConfig.fetchConnectionCache = true;
9 | const sql = neon(Config.DATABASE_URL)
10 | if (useSQLOnly) {
11 | return sql
12 | }
13 | return drizzle(sql);
14 | }
15 |
16 | export async function dbNow () {
17 | const sql = await dbClient(true)
18 | const dbResult = await sql`SELECT NOW()`
19 | if (dbResult.length === 1) {
20 | return dbResult[0].now
21 | }
22 | return null
23 | }
24 |
25 | export async function addLead({email}){
26 | const db = await dbClient(false)
27 | const dbResult = await db.insert(schema.LeadTable).values({email: email}).returning({timestamp: schema.LeadTable.createdAt})
28 | if (dbResult.length === 1) {
29 | return dbResult[0].timestamp
30 | }
31 | return
32 | }
--------------------------------------------------------------------------------
/src/app/leadCapture/hero.js:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import LeadCaptureForm from '@/app/leadCapture/forms'
3 | export default function HeroLandingPage () {
4 |
5 | return
6 |
7 |
8 |
Landing Page for Sales Leads!
9 |
Using Next.js to deploy to AWS with Neon
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | }
--------------------------------------------------------------------------------
/src/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "id": "49b183d1-80b3-43ec-9af7-84da8b213d1d",
5 | "prevId": "00000000-0000-0000-0000-000000000000",
6 | "tables": {
7 | "leads": {
8 | "name": "leads",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "email": {
18 | "name": "email",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "created_at": {
24 | "name": "created_at",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": false,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {},
33 | "compositePrimaryKeys": {},
34 | "uniqueConstraints": {}
35 | }
36 | },
37 | "enums": {},
38 | "schemas": {},
39 | "_meta": {
40 | "schemas": {},
41 | "tables": {},
42 | "columns": {}
43 | }
44 | }
--------------------------------------------------------------------------------
/src/migrations/meta/0002_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "id": "4c7bd463-f831-45f3-89fd-bf8c203ad26f",
5 | "prevId": "6cf51274-700e-4ec6-ba20-dbcbd458c7eb",
6 | "tables": {
7 | "leads": {
8 | "name": "leads",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "email": {
18 | "name": "email",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "created_at": {
24 | "name": "created_at",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": false,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {},
33 | "compositePrimaryKeys": {},
34 | "uniqueConstraints": {}
35 | }
36 | },
37 | "enums": {},
38 | "schemas": {},
39 | "_meta": {
40 | "schemas": {},
41 | "tables": {},
42 | "columns": {}
43 | }
44 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "leads-landing",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "sst bind next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "generate": "drizzle-kit generate:pg --config=drizzle.config.js",
11 | "migrate": "tsx src/app/cli/migrator.js",
12 | "deployStaging": "sst deploy --stage staging",
13 | "deploy": "sst deploy --stage production"
14 | },
15 | "dependencies": {
16 | "@neondatabase/serverless": "^0.6.0",
17 | "drizzle-orm": "^0.28.6",
18 | "drizzle-zod": "^0.5.1",
19 | "next": "13.5.4",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "validator": "^13.11.0",
23 | "zod": "^3.22.4",
24 | "zod-validation-error": "^1.5.0"
25 | },
26 | "devDependencies": {
27 | "autoprefixer": "^10.4.16",
28 | "aws-cdk-lib": "2.95.1",
29 | "aws-sdk": "^2.1473.0",
30 | "constructs": "10.2.69",
31 | "drizzle-kit": "^0.19.13",
32 | "eslint": "^8.51.0",
33 | "eslint-config-next": "13.5.4",
34 | "postcss": "^8.4.31",
35 | "sst": "^2.29.0",
36 | "tailwindcss": "^3.3.3",
37 | "tsx": "^3.13.0"
38 | }
39 | }
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | /* body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | } */
28 |
29 | @layer base {
30 | input[type='email'] {
31 | @apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500;
32 | }
33 |
34 | .btn-join {
35 | @apply text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: One-Off Deploy via SST to AWS
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | - uses: actions/setup-node@v3
12 | with:
13 | node-version: '18.x'
14 | - name: Cache Next.js Build
15 | uses: actions/cache@v3
16 | with:
17 | path: |
18 | .next/cache/
19 | .open-next/
20 | .sst/
21 | key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]xs') }}
22 | restore-keys: |
23 | cache-${{ hashFiles('**/pnpm-lock.yaml') }}-
24 | - name: Install Pnpm
25 | run: npm install -g pnpm
26 |
27 | - name: Install projects deps
28 | run: pnpm install
29 |
30 | - name: Install AWS Creds
31 | run: |
32 | mkdir -p ~/.aws
33 | echo "[default]" > ~/.aws/credentials
34 | echo "aws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}" >> ~/.aws/credentials
35 | echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials
36 |
37 | - name: Set SST Config Secret
38 | run: |
39 | npx sst secrets set COMMIT_SHA '${{ github.sha }}' --stage production
40 |
41 | - name: Deploy to AWS with SST
42 | run: pnpm run deploy
43 |
44 | - name: Clean Up AWS Profile
45 | run: |
46 | rm -rf ~/.aws
--------------------------------------------------------------------------------
/src/migrations/meta/0001_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "id": "6cf51274-700e-4ec6-ba20-dbcbd458c7eb",
5 | "prevId": "49b183d1-80b3-43ec-9af7-84da8b213d1d",
6 | "tables": {
7 | "leads": {
8 | "name": "leads",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "email": {
18 | "name": "email",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "first_name": {
24 | "name": "first_name",
25 | "type": "varchar(150)",
26 | "primaryKey": false,
27 | "notNull": false
28 | },
29 | "last_name": {
30 | "name": "last_name",
31 | "type": "varchar(150)",
32 | "primaryKey": false,
33 | "notNull": false
34 | },
35 | "created_at": {
36 | "name": "created_at",
37 | "type": "timestamp",
38 | "primaryKey": false,
39 | "notNull": false,
40 | "default": "now()"
41 | }
42 | },
43 | "indexes": {},
44 | "foreignKeys": {},
45 | "compositePrimaryKeys": {},
46 | "uniqueConstraints": {}
47 | }
48 | },
49 | "enums": {},
50 | "schemas": {},
51 | "_meta": {
52 | "schemas": {},
53 | "tables": {},
54 | "columns": {}
55 | }
56 | }
--------------------------------------------------------------------------------
/src/migrations/meta/0003_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "id": "3929ed22-e2af-40e9-8cb7-ceefe94aea34",
5 | "prevId": "4c7bd463-f831-45f3-89fd-bf8c203ad26f",
6 | "tables": {
7 | "leads": {
8 | "name": "leads",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "email": {
18 | "name": "email",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "first_name": {
24 | "name": "first_name",
25 | "type": "varchar(150)",
26 | "primaryKey": false,
27 | "notNull": false
28 | },
29 | "last_name": {
30 | "name": "last_name",
31 | "type": "varchar(150)",
32 | "primaryKey": false,
33 | "notNull": false
34 | },
35 | "created_at": {
36 | "name": "created_at",
37 | "type": "timestamp",
38 | "primaryKey": false,
39 | "notNull": false,
40 | "default": "now()"
41 | }
42 | },
43 | "indexes": {},
44 | "foreignKeys": {},
45 | "compositePrimaryKeys": {},
46 | "uniqueConstraints": {}
47 | }
48 | },
49 | "enums": {},
50 | "schemas": {},
51 | "_meta": {
52 | "schemas": {},
53 | "tables": {},
54 | "columns": {}
55 | }
56 | }
--------------------------------------------------------------------------------
/src/app/cli/migrator.js:
--------------------------------------------------------------------------------
1 |
2 | import { migrate } from "drizzle-orm/postgres-js/migrator";
3 | import { drizzle } from 'drizzle-orm/neon-serverless';
4 | import { Pool, neonConfig } from '@neondatabase/serverless';
5 |
6 | import * as schema from "../lib/schema";
7 | import getSecret from "../lib/secrets";
8 |
9 |
10 | import ws from 'ws';
11 |
12 |
13 |
14 | async function performMigration() {
15 | const dbUrl = await getSecret("DATABASE_URL")
16 | if (!dbUrl) {
17 | return
18 | }
19 | // connect to Neon via websocket
20 |
21 | neonConfig.webSocketConstructor = ws; // <-- this is the key bit
22 |
23 | const pool = new Pool({ connectionString: dbUrl });
24 | pool.on('error', err => console.error(err)); // deal with e.g. re-connect
25 |
26 | const client = await pool.connect();
27 |
28 | try {
29 | await client.query('BEGIN');
30 | const db = await drizzle(client, {schema})
31 | await migrate(db, { migrationsFolder: "src/migrations" });
32 | await client.query('COMMIT');
33 |
34 | } catch (err) {
35 | await client.query('ROLLBACK');
36 | throw err;
37 |
38 | } finally {
39 | client.release();
40 | }
41 |
42 | await pool.end();
43 |
44 | }
45 |
46 | if (require.main === module) {
47 | console.log("Running migrations")
48 |
49 | performMigration().then((val)=>{
50 | console.log("Migration performed")
51 | process.exit(0)
52 | }).catch((err)=>{
53 | console.log("err", err)
54 | process.exit(1)
55 | })
56 | }
--------------------------------------------------------------------------------
/src/app/leadCapture/forms.js:
--------------------------------------------------------------------------------
1 | "use client"
2 | import {useState, useRef} from 'react'
3 |
4 | export default function LeadCaptureForm () {
5 | const [loading, setLoading] = useState(false)
6 | const [message, setMessage] = useState('')
7 | const formRef = useRef(null)
8 |
9 | const handleForm = async (event) => {
10 | event.preventDefault()
11 | setLoading(true)
12 | const formData = new FormData(event.target)
13 |
14 | const dataObject = Object.fromEntries(formData)
15 |
16 | const jsonData = JSON.stringify(dataObject)
17 |
18 | const options = {
19 | method: "POST", // HTTP POST
20 | headers: {
21 | "Content-Type": "application/json"
22 | },
23 | body: jsonData
24 | }
25 |
26 | // fetch
27 | const response = await fetch("/api/leads/", options)
28 | // const responseData = await response.json()
29 | if (response.ok) {
30 | setMessage("Thank you for joining")
31 | formRef.current.reset()
32 | } else {
33 | setMessage("Error with your request")
34 | }
35 | setLoading(false)
36 |
37 | }
38 | const btnLabel = loading ? "Loading" : "Join List"
39 | return <>
40 | {message && {message}
}
41 |
45 | >
46 | }
--------------------------------------------------------------------------------
/src/app/api/leads/route.js:
--------------------------------------------------------------------------------
1 | import {NextResponse} from 'next/server'
2 | // import validator from 'validator'
3 | import {z as zod} from 'zod'
4 | import {fromZodError} from 'zod-validation-error'
5 |
6 | import * as db from '@/app/lib/db'
7 | import * as schema from '@/app/lib/schema'
8 |
9 | export const dynamic = 'force-dynamic'
10 | export const runtime = 'nodejs'
11 | export const fetchCache = 'force-no-store'
12 | export const revalidate = 0
13 |
14 | export async function POST(request){ // HTTP POST
15 | const contentType = await request.headers.get("content-type")
16 | if (contentType !== "application/json") {
17 | return NextResponse.json({message: "Invalid request"}, {status: 415})
18 | }
19 | const data = await request.json()
20 | let parsedData = {}
21 | try {
22 | parsedData = await schema.insertLeadTableSchema.parse(data)
23 | } catch (error) {
24 | if (error instanceof zod.ZodError) {
25 | const validationError = fromZodError(error)
26 | return NextResponse.json({errorList: validationError}, {status: 400})
27 | }
28 | return NextResponse.json({message: "Some Server Error"}, {status: 500})
29 | }
30 | const {email} = parsedData
31 | if (!email) {
32 | return NextResponse.json({message: "A valid email is required"}, {status: 400})
33 | }
34 | const dbNow = await db.dbNow()
35 | const leadResult = await db.addLead({email: email})
36 |
37 | const resultData = {
38 | leadResult: leadResult,
39 | dbNow: dbNow
40 | }
41 | return NextResponse.json(resultData, {status: 201})
42 |
43 | }
--------------------------------------------------------------------------------
/src/migrations/meta/0004_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "id": "016f2609-1992-4c2a-b7ed-72597b1fc44b",
5 | "prevId": "3929ed22-e2af-40e9-8cb7-ceefe94aea34",
6 | "tables": {
7 | "leads": {
8 | "name": "leads",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "email": {
18 | "name": "email",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "first_name": {
24 | "name": "first_name",
25 | "type": "varchar(150)",
26 | "primaryKey": false,
27 | "notNull": false
28 | },
29 | "last_name": {
30 | "name": "last_name",
31 | "type": "varchar(150)",
32 | "primaryKey": false,
33 | "notNull": false
34 | },
35 | "description": {
36 | "name": "description",
37 | "type": "text",
38 | "primaryKey": false,
39 | "notNull": false
40 | },
41 | "created_at": {
42 | "name": "created_at",
43 | "type": "timestamp",
44 | "primaryKey": false,
45 | "notNull": false,
46 | "default": "now()"
47 | }
48 | },
49 | "indexes": {},
50 | "foreignKeys": {},
51 | "compositePrimaryKeys": {},
52 | "uniqueConstraints": {}
53 | }
54 | },
55 | "enums": {},
56 | "schemas": {},
57 | "_meta": {
58 | "schemas": {},
59 | "tables": {},
60 | "columns": {}
61 | }
62 | }
--------------------------------------------------------------------------------
/.github/workflows/production.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy Production via SST to AWS
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - production
7 | types:
8 | - closed
9 | # workflow_dispatch:
10 |
11 | jobs:
12 | production:
13 | if: github.event.pull_request.merged == true
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: '18.x'
20 | - name: Cache Next.js Build
21 | uses: actions/cache@v3
22 | with:
23 | path: |
24 | .next/cache/
25 | .open-next/
26 | .sst/
27 | key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]xs') }}
28 | restore-keys: |
29 | cache-${{ hashFiles('**/pnpm-lock.yaml') }}-
30 | - name: Install Pnpm
31 | run: npm install -g pnpm
32 |
33 | - name: Install projects deps
34 | run: pnpm install
35 |
36 | - name: Install AWS Creds
37 | run: |
38 | mkdir -p ~/.aws
39 | echo "[default]" > ~/.aws/credentials
40 | echo "aws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}" >> ~/.aws/credentials
41 | echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials
42 |
43 | - name: Set SST Config Secret
44 | run: |
45 | npx sst secrets set COMMIT_SHA '${{ github.sha }}' --stage production
46 |
47 | - name: Production Database Migrations
48 | run: |
49 | pnpm run migrate
50 |
51 | - name: Deploy to AWS with SST
52 | run: pnpm run deploy
53 |
54 | - name: Clean Up AWS Profile
55 | run: |
56 | rm -rf ~/.aws
57 |
58 | # - name: Remove Staging Deployment
59 | # run: |
60 | # npx sst remove --stage staging
--------------------------------------------------------------------------------
/.github/workflows/stage.yaml:
--------------------------------------------------------------------------------
1 | name: Staging Deploy via SST to AWS
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - "main"
8 | - "master"
9 | paths:
10 | - "src/**"
11 | - "*.json"
12 | - "*.yaml"
13 | - "*.json"
14 | - "*.config.js"
15 | - "*.config.ts"
16 |
17 | jobs:
18 | deploy:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v3
22 | - uses: actions/setup-node@v3
23 | with:
24 | node-version: '18.x'
25 | - name: Cache Next.js Build
26 | uses: actions/cache@v3
27 | with:
28 | path: |
29 | .next/cache/
30 | .open-next/
31 | .sst/
32 | key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]xs') }}
33 | restore-keys: |
34 | cache-${{ hashFiles('**/pnpm-lock.yaml') }}-
35 | - name: Install Pnpm
36 | run: npm install -g pnpm
37 |
38 | - name: Install projects deps
39 | run: pnpm install
40 |
41 | - name: Install AWS Creds
42 | run: |
43 | mkdir -p ~/.aws
44 | echo "[default]" > ~/.aws/credentials
45 | echo "aws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}" >> ~/.aws/credentials
46 | echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials
47 |
48 | - name: Set SST Config Secret
49 | run: |
50 | npx sst secrets set DATABASE_URL '${{ secrets.DATABASE_URL_STAGING }}' --stage staging
51 | npx sst secrets set SECRET_VAL '${{ github.sha }}' --stage staging
52 | npx sst secrets set COMMIT_SHA '${{ github.sha }}' --stage staging
53 |
54 | - name: Run Staging DB migrations
55 | run: |
56 | STAGE=staging npx tsx src/app/cli/migrator.js
57 |
58 | - name: Deploy to AWS with SST
59 | run: pnpm run deployStaging
60 |
61 | - name: Clean Up AWS Profile
62 | run: |
63 | rm -rf ~/.aws
64 |
65 | - name: Create Pull Request to Production
66 | if: success()
67 | env:
68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69 | run:
70 | gh pr create --base production --head main --title "Auto- ${{ github.event.head_commit.message }}" --body "Auto PR from Stage Release"
--------------------------------------------------------------------------------