ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
Cooachly connects students with professors for scheduled 1:1 coaching sessions, messaging, and payments — across any timezone.
EducationTypeScript · JavaScriptGitHub @Balachan016OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
A coaching platform with three roles — admin, professor, and student — built with
Next.js (App Router), TypeScript, Prisma/PostgreSQL, and Stripe.Read the full README ↗A coaching platform with three roles — admin, professor, and student — built with Next.js (App Router), TypeScript, Prisma/PostgreSQL, and Stripe.
src/proxy.ts and in every server component via
src/lib/dal.ts).DAILY_API_KEY isn't set, professors can paste a manual meeting link (Zoom, Google Meet, etc.)
instead.DAILY_ENABLE_TRANSCRIPTION="true"
to transcribe sessions live (Daily + Deepgram, billed per participant-minute — no audio/video is
stored, just text) and get them summarized (OpenAI GPT) automatically after each class, then
emailed to both student and professor. Off by default to keep video calls free./api/cron/reminders./about.jose + bcryptjs) — no third-party auth provider requiredEvery third-party integration above is optional at the code level — if its API key isn't set, that feature no-ops (logs to the console) instead of crashing, so you can run and demo the app without signing up for anything, then turn integrations on one at a time.
npm install
Copy .env.example to .env and fill in the values:
cp .env.example .env
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string |
SESSION_SECRET | Random secret used to sign session cookies. Generate with openssl rand -base64 32 |
NEXT_PUBLIC_APP_URL | Public URL of the app (http://localhost:3000 locally) |
STRIPE_SECRET_KEY | Stripe secret key (optional in dev — see below) |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Stripe publishable key |
STRIPE_WEBHOOK_SECRET | Signing secret for the /api/stripe/webhook endpoint |
If you leave the Stripe keys as placeholders, the app runs in test mode: bookings are auto-confirmed without collecting payment, so you can build and demo the whole flow without a Stripe account.
You need a PostgreSQL database. Locally you can use Docker:
docker run --name cooachly-db -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cooachly -p 5432:5432 -d postgres:16
Then point DATABASE_URL at it (the default in .env.example already matches this setup), and
run the migrations:
npm run db:migrate
npm run db:seed
This creates three accounts you can log in with immediately:
| Role | Password | |
|---|---|---|
| Admin | [email protected] | ChangeMe123! |
| Professor | [email protected] | ChangeMe123! |
| Student | [email protected] | ChangeMe123! |
Change or remove these before going to production.
npm run dev
Open http://localhost:3000.
Create a free account at stripe.com.
Copy your test publishable and secret keys from
dashboard.stripe.com/test/apikeys into .env.
To receive webhook events locally, install the Stripe CLI and run:
stripe listen --forward-to localhost:3000/api/stripe/webhook
Copy the whsec_... value it prints into STRIPE_WEBHOOK_SECRET.
In production, add a webhook endpoint in the Stripe dashboard pointing at
https://your-domain.com/api/stripe/webhook, subscribed to at least:
checkout.session.completed, customer.subscription.updated, customer.subscription.deleted.
Email — Resend: sign up at resend.com, verify a sending domain (or
use their [email protected] test address to start), create an API key at
resend.com/api-keys, and set RESEND_API_KEY +
RESEND_FROM_EMAIL.
WhatsApp — Twilio: sign up at twilio.com, join Twilio's WhatsApp
sandbox (or apply for a production WhatsApp sender), and set TWILIO_ACCOUNT_SID,
TWILIO_AUTH_TOKEN, and TWILIO_WHATSAPP_FROM to the number Twilio gives you, e.g.
whatsapp:+14155238886.
Reminders, invites, and demo confirmations go out by both WhatsApp and SMS — a recipient who misses one channel (no WhatsApp installed, a silent WhatsApp-side rejection) still gets the other. SMS doesn't need a pre-approved Content Template the way WhatsApp does, but each country's SMS leg needs its own locally-registered sender number first (see US/Canada & India SMS below) — leave a country's sender unset to keep sending WhatsApp-only to that country until it's registered.
Before going live, submit each WhatsApp message type below as its own template for Meta's approval in the Twilio console (Messaging → Content Editor). Meta requires an approved template for any business-initiated message sent outside a 24-hour customer-service window — which reminders, invites, and demo confirmations always are — so free-form messages are rejected once you're off the sandbox, with Twilio error 21654 "ContentSid Required".
Submit templates that look like this (Category: Utility, so none need marketing opt-in):
TWILIO_REMINDER_CONTENT_SID:
Hi {{1}}, this is a reminder that your Cooachly session with {{2}} starts {{3}} ({{4}}).
Join here: {{5}}
TWILIO_INVITE_CONTENT_SID:
Hi {{1}}, you've been invited to create your {{2}} account. Create your login here: {{3}}
TWILIO_DEMO_CONTENT_SID:
Thanks for requesting a free {{2}} demo for {{3}}, {{1}}! We'll reach out shortly to
schedule a time.
Reminders only go out for confirmed bookings, and only once per threshold (tracked via
reminder24hSentAt / reminder1hSentAt / reminder5mSentAt on each booking), so re-running the
job frequently is safe. WhatsApp and SMS are both skipped for anyone without a phone number on file.
The app ships a web app manifest per site (/manifest.webmanifest and /arts/manifest.webmanifest)
and a service worker (public/sw.js), so on any HTTPS deployment it can be installed straight from
the browser — no App Store or Play Store submission needed:
Push notifications (session reminders at 1 day / 1 hour / 5 minutes, and new messages) need a VAPID key pair. Generate it once and add both values to your environment:
npx web-push generate-vapid-keys
| Variable | Description |
|---|---|
NEXT_PUBLIC_VAPID_PUBLIC_KEY | Public VAPID key (sent to browsers) |
VAPID_PRIVATE_KEY | Private VAPID key (server only) |
VAPID_SUBJECT | mailto: address push services can contact you at |
Keep the same keys forever — rotating them silently breaks every existing device subscription. Without them, push is skipped and reminders still go out by email/WhatsApp. Each user then turns on notifications per device under Settings → Mobile app & notifications; the admin Send test notification button also sends a test push. The service worker never caches pages or data (so nobody sees stale bookings); it only shows an offline page when there's no connection.
DAILY_API_KEY.
Once set, every confirmed booking automatically gets a video room — no code changes needed.
Without it, professors fall back to pasting a manual meeting link.https://your-domain.com/api/daily/webhook, subscribed to participant.joined and
participant.left. Every "Join video call" link in the app already routes through
/api/daily/join/[bookingId], which tags the join with the signed-in user's own account before
sending them to the room, so each join/leave is correctly attributed to that student or
professor — not just an anonymous Daily participant. The full join/leave history (count and
every timestamp, per person) shows on that booking's row under Admin → Class logs.DAILY_ENABLE_TRANSCRIPTION unset or false), video calls
work but aren't transcribed, and no summary is generated. To turn transcription + summaries on:
DAILY_API_KEY and is required before transcription will work at all).DAILY_ENABLE_TRANSCRIPTION="true".https://your-domain.com/api/daily/webhook, subscribed to both the meeting.started event
(starts transcription when a call begins) and the transcript.ready-to-download event
(fetches the finished transcript). (Optional: enable webhook signing and set
DAILY_WEBHOOK_SECRET to verify requests.)OPENAI_API_KEY. This powers the GPT-generated summary that gets emailed to both parties
after each session, and the monthly student recaps admins can generate from Class Logs.No audio or video is ever stored with this approach — only the text transcript, which is both cheaper and more private than cloud recording.
If attendance tracking, transcription, and AI summaries all stop at once (not just one of them), the webhook has almost certainly tripped Daily's circuit breaker: by default, 3 non-2xx responses from your endpoint — across any event type — and Daily stops sending all future webhooks to it, silently, with no notice anywhere in your own app. Check for this first:
GET https://api.daily.co/v1/webhooks (with your DAILY_API_KEY as the bearer token) lists
your webhook(s) — look at failedCount and whether it's still trying to deliver./api/daily/webhook around
when it stopped, and confirm DAILY_WEBHOOK_SECRET in production still matches what Daily has
on file for that webhook, if you set one — a mismatch after recreating/updating the webhook
rejects every single delivery with 401 and trips the breaker almost immediately).POST https://api.daily.co/v1/webhooks/<uuid> makes Daily retry once; a 200 from your endpoint
closes the breaker and resumes normal delivery. There's no dashboard button for this — it's
API-only.Professors/admins can share a test file on a booking, and students can upload their answer back. This needs blob storage:
BLOB_READ_WRITE_TOKEN into your production environment once
connected — no manual copying needed there. For local development, copy the token shown in the
Blob store's dashboard into your .env.Without this configured, upload attempts show a message asking an admin to set it up, rather than failing silently.
cooachly.comYou already own the domain; the pieces you need are (1) somewhere to run the Next.js app, and (2) a Postgres database, then (3) pointing the domain at the app. The easiest and cheapest path:
If it isn't already, push this project to a GitHub repository — Vercel deploys straight from Git.
Any of these work well and have a free tier:
Create a database and copy its connection string — you'll use it as DATABASE_URL.
Go to vercel.com/new and import this GitHub repository.
Vercel auto-detects Next.js — no build settings to change.
Add the environment variables from your .env (DATABASE_URL, SESSION_SECRET,
NEXT_PUBLIC_APP_URL, STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
STRIPE_WEBHOOK_SECRET) under Project Settings → Environment Variables.
NEXT_PUBLIC_APP_URL to https://cooachly.com (or the Vercel URL until your domain is
connected).Deploy. Vercel will run npm run build, which runs prisma generate automatically
(see the postinstall/build scripts in package.json).
No deployment showing up? Vercel only builds on new pushes made after the Git connection is created — it won't retroactively build a commit that already existed on
mainbefore you connected the repo. If Deployments is empty, push any new commit tomain(or reconnect the repo under Project Settings → Git) to trigger the first build.
After the first deploy, run the database migration against your production database once
(from your machine, with DATABASE_URL set to the production connection string):
npm run db:migrate
(Or use prisma migrate deploy in a CI step for subsequent deploys.)
cooachly.com (and www.cooachly.com
if you want both).cooachly.com): an A record pointing to 76.76.21.21
(Vercel will show the exact current value — use what it gives you).www.cooachly.com: a CNAME record pointing to cname.vercel-dns.com.cooachly.com or www.cooachly.com is canonical, and set up a redirect for the
other in Vercel's domain settings (it offers a one-click "redirect to" option).In the Stripe dashboard, add an endpoint at
https://cooachly.com/api/stripe/webhook and copy its signing secret into Vercel's
STRIPE_WEBHOOK_SECRET environment variable. Switch your Stripe API keys from test to live mode
when you're ready to accept real payments.
That's it — cooachly.com will serve the app, backed by your Postgres database, with Stripe
handling payments.
prisma/schema.prisma Database schema (User, ProfessorProfile, Availability, Booking, Message, Subscription, Attachment, MonthlySummary)
prisma/seed.ts Seed script for sample admin/professor/student accounts
src/proxy.ts Route protection (Next.js 16's replacement for middleware.ts)
src/lib/ Session/auth, Prisma client, scheduling, Stripe, Daily.co, AI summary, blob storage, DAL helpers
src/lib/notifications/ Email (Resend) + WhatsApp/SMS (Twilio) senders
src/actions/ Server Actions (auth, bookings, availability, messages, billing, admin, profile, attachments, monthly-summary)
src/app/(admin|professor|student)/ Role-specific dashboards
src/app/admin/class-logs Admin view of every past session's transcript/AI summary + monthly recaps
src/app/about Public "About Cooachly" page
src/app/api/stripe/webhook Stripe webhook handler
src/app/api/daily/webhook Daily.co webhook → attendance events, transcription, summarize + email
src/app/api/daily/join Tags a join with the signed-in user before sending them to the room
src/app/api/cron/reminders Scheduled job: sends 24h/1h/5m reminders for upcoming sessions
Once Meta approves each one, copy its Content SID (starts with HX...) into the matching
env var above. Until an env var is set, that message type sends free-form text, which only
works on the sandbox and will keep failing with 21654 on a real WhatsApp sender.
US/Canada & India SMS: each country needs its own locally-registered sender number before Twilio/local carriers will deliver SMS to it:
TWILIO_SMS_FROM_US to that number, e.g.
+14155550100 (no whatsapp: prefix).TWILIO_SMS_FROM_INDIA to that number, e.g. +910000000000.Once a country's sender is set, every recipient with that country's phone number automatically gets both the SMS and the WhatsApp send — the same free-text body each message type already builds, no content template needed for SMS. Until a country's env var is set, its recipients keep getting WhatsApp-only, same as before this was added.
Delivery status: Twilio's API only confirms it accepted a message, not that WhatsApp/SMS
actually delivered it — a "SENT" row in the reminder log can still silently fail to arrive.
Every WhatsApp/SMS send now registers a per-message statusCallback to /api/twilio/status
(nothing to configure in the Twilio console — it's passed automatically), which records the
real outcome (queued/sent/delivered/read/undelivered/failed, plus Twilio's error code if any)
back onto that row. Check Admin → Bookings' expanded "Reminder log" or Superadmin → Reminders
for the delivery badge next to the usual status one.
Scheduling the reminder job: set CRON_SECRET to a random string, then use a free
external scheduler like cron-job.org to call
https://your-domain.com/api/cron/reminders?secret=<CRON_SECRET> every 5 minutes.
This step is easy to miss — without it, /api/cron/reminders is never called by anything,
so no reminder (and no entry in Admin → Bookings' "Reminders" log) will ever appear, even with
email/WhatsApp fully configured. Check Admin → Bookings after setting this up: you should start
seeing "Reminders" entries appear within a few minutes of a session's 24h/1h/5m mark.
(Vercel's own Cron Jobs feature can also call this endpoint via a vercel.json crons entry,
but Vercel's Hobby plan only allows once-a-day cron schedules — a 5-minute schedule is
silently rejected and will block every deployment. Stick with an external scheduler like
cron-job.org unless you're on a Pro plan or above.)