Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Neon Postgres connection string (Project Settings → Connection Details on neon.tech)
# Use the POOLED connection (host contains `-pooler`) — required for serverless/Netlify
DATABASE_URL=postgresql://USER:PASSWORD@HOST/DBNAME?sslmode=require

# NextAuth JWT signing secret — generate with: openssl rand -base64 32
NEXTAUTH_SECRET=

# Public site URL — MUST include the https:// scheme in production
# (NextAuth concatenates this with paths for redirects/cookies; no scheme = malformed URLs)
NEXTAUTH_URL=http://localhost:3000

# Gmail SMTP credentials for transactional email
# GMAIL_USER is the sender mailbox; GMAIL_APP_PASSWORD is a 16-char app password
# generated at https://myaccount.google.com/apppasswords (2FA required on the account)
GMAIL_USER=
GMAIL_APP_PASSWORD=

# Bearer token shared between the Netlify scheduled functions and the Next.js
# /api/cron/* routes — generate with: openssl rand -base64 32
CRON_SECRET=

# Optional: timezone used for display formatting (defaults to America/Chicago)
APP_TIMEZONE=America/Chicago
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ yarn-error.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# vercel
.vercel
Expand Down
3 changes: 3 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[functions]
directory = "netlify/functions"
node_bundler = "esbuild"
31 changes: 31 additions & 0 deletions netlify/functions/close-expired-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Config } from "@netlify/functions";

export default async (): Promise<Response> => {
const baseUrl = process.env.URL;
const secret = process.env.CRON_SECRET;

if (!baseUrl || !secret) {
console.error(
"[close-expired-events] Missing URL or CRON_SECRET env var; skipping run",
);
return new Response("Missing required env vars", { status: 500 });
}

const response = await fetch(`${baseUrl}/api/cron/close-expired-events`, {
headers: { Authorization: `Bearer ${secret}` },
});

if (!response.ok) {
const body = await response.text();
console.error(
`[close-expired-events] Upstream returned ${response.status}: ${body}`,
);
return new Response(`Upstream failed: ${response.status}`, { status: 502 });
}

return new Response("OK");
};

export const config: Config = {
schedule: "*/30 * * * *",
};
31 changes: 31 additions & 0 deletions netlify/functions/event-reminders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Config } from "@netlify/functions";

export default async (): Promise<Response> => {
const baseUrl = process.env.URL;
const secret = process.env.CRON_SECRET;

if (!baseUrl || !secret) {
console.error(
"[event-reminders] Missing URL or CRON_SECRET env var; skipping run",
);
return new Response("Missing required env vars", { status: 500 });
}

const response = await fetch(`${baseUrl}/api/cron/event-reminders`, {
headers: { Authorization: `Bearer ${secret}` },
});

if (!response.ok) {
const body = await response.text();
console.error(
`[event-reminders] Upstream returned ${response.status}: ${body}`,
);
return new Response(`Upstream failed: ${response.status}`, { status: 502 });
}

return new Response("OK");
};

export const config: Config = {
schedule: "0 8 * * *",
};
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"check": "concurrently --kill-others-on-fail 'pnpm lint' 'tsc --noEmit'"
"check": "concurrently --kill-others-on-fail 'pnpm lint' 'tsc --noEmit'",
"netlify:dev": "netlify dev"
},
"dependencies": {
"@emotion/cache": "^11.14.0",
Expand All @@ -17,6 +18,7 @@
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/list": "^6.1.20",
"@fullcalendar/react": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@hookform/resolvers": "^5.2.1",
Expand All @@ -26,25 +28,26 @@
"@mui/x-charts": "^8.27.5",
"@mui/x-data-grid": "^8.10.0",
"@neondatabase/serverless": "^1.0.1",
"@netlify/blobs": "^10.7.6",
"@netlify/functions": "^5.2.1",
"@tiptap/extension-color": "^3.16.0",
"@tiptap/extension-link": "^3.16.0",
"@tiptap/extension-text-style": "^3.16.0",
"@tiptap/extension-underline": "^3.16.0",
"@tiptap/pm": "^3.16.0",
"@tiptap/react": "^3.16.0",
"@tiptap/starter-kit": "^3.16.0",
"@vercel/blob": "^2.3.1",
"bcrypt": "^6.0.0",
"dotenv": "^17.2.1",
"drizzle-orm": "^0.44.4",
"next": "15.5.9",
"next-auth": "^4.24.12",
"nodemailer": "^8.0.7",
"notistack": "^3.0.2",
"papaparse": "^5.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"resend": "^6.5.0",
"validator": "^13.15.23",
"zod": "^4.0.17"
},
Expand All @@ -54,6 +57,7 @@
"@next/eslint-plugin-next": "^15.4.7",
"@types/bcrypt": "^6.0.0",
"@types/node": "^20",
"@types/nodemailer": "^8.0.0",
"@types/papaparse": "^5.5.2",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand All @@ -68,6 +72,7 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unicorn": "^60.0.0",
"knip": "^5.62.0",
"netlify-cli": "^26.0.1",
"prettier": "^3.6.2",
"tsx": "^4.20.3",
"typescript": "latest",
Expand Down
Loading
Loading