## 2. Set up Stripe (payments) 1. Create a free account at https://dashboard.stripe.com 2. Go to Developers → API keys and copy your Secret key into STRIPE_SECRET_KEY 3. Go to Developers → Webhooks → Add endpoint, set the URL to https://yourdomain.com/api/stripe-webhook, select event checkout.session.completed, then copy the Signing secret into STRIPE_WEBHOOK_SECRET 4. Start in test mode (test card: 4242 4242 4242 4242, any future date/CVC) and switch to live keys once ready ## 3. Set up email sending 1. Turn on 2-Step Verification on the Gmail account you'll send from 2. Create an App Password: Google Account → Security → 2-Step Verification → App passwords 3. Put that 16-character password in EMAIL_APP_PASSWORD (not your normal Gmail password) ## 4. Configure .env# Sprout & Sum Tutoring — Website + Backend ## What's included - `public/index.html` — the full website (front-end) - `public/booking-success.html` — page shown after a parent pays - `server.js` — Express backend: available time slots, bookings, Stripe payment, email confirmations - `package.json` — dependencies - `.env.example` — copy to `.env` and fill in your real keys ## 1. Install Requires Node.js 18+. # Sprout & Sum Tutoring — Website + Backend ## What's included - `public/index.html` — the full website (front-end) - `public/booking-success.html` — page shown after a parent pays - `server.js` — Express backend: available time slots, bookings, Stripe payment, email confirmations - `package.json` — dependencies - `.env.example` — copy to `.env` and fill in your real keys ## 1. Install Requires Node.js 18+. Booking Confirmed — Sprout & Sum Tutoring

Payment received 🎉

Thank you! Your session is booked. A confirmation email with your lesson link is on its way to your inbox.

If it doesn't arrive within a few minutes, check spam or email eshiphangitu@gmail.com.

Back to homepage
{ "name": "sprout-and-sum-tutoring", "version": "1.0.0", "private": true, "description": "Backend for the Sprout & Sum tutoring website: bookings, Stripe payments, and email confirmations.", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "better-sqlite3": "^11.3.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "nodemailer": "^6.9.14", "stripe": "^16.8.0" } } # Copy this file to ".env" and fill in your real values. Never commit ".env" to git. # The port your server runs on locally (hosts like Render/Railway set this for you) PORT=3000 # The public URL of your deployed site (used for Stripe redirect links) SITE_URL=http://localhost:3000 # From your Stripe Dashboard -> Developers -> API keys STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx # From your Stripe Dashboard -> Developers -> Webhooks (after you add the webhook endpoint) STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx # The Gmail address that sends/receives booking + contact emails EMAIL_USER=eshiphangitu@gmail.com # A Gmail "App Password" (NOT your normal Gmail password) — see README for how to create one EMAIL_APP_PASSWORD=xxxxxxxxxxxxxxxx # A long random string only you know, used to protect the /api/admin/* routes ADMIN_KEY=choose-a-long-random-string-here # Where confirmed lessons will actually happen — a standing Zoom or Google Meet link DEFAULT_MEETING_LINK=https://meet.google.com/your-link require('dotenv').config(); const express = require('express'); const path = require('path'); const Database = require('better-sqlite3'); const Stripe = require('stripe'); const nodemailer = require('nodemailer'); const cors = require('cors'); const app = express(); const PORT = process.env.PORT || 3000; const stripe = Stripe(process.env.STRIPE_SECRET_KEY); const SESSION_PRICE_CENTS = 2500; // $25.00 per 1-hour session // --------------------------------------------------------------------------- // Database (SQLite file, created automatically on first run) // --------------------------------------------------------------------------- const db = new Database(path.join(__dirname, 'tutoring.db')); db.pragma('journal_mode = WAL'); db.exec(` CREATE TABLE IF NOT EXISTS slots ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time_utc TEXT NOT NULL, booked INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS bookings ( id INTEGER PRIMARY KEY AUTOINCREMENT, slot_id INTEGER, parent_name TEXT NOT NULL, parent_email TEXT NOT NULL, child_name TEXT NOT NULL, child_age INTEGER NOT NULL, subject TEXT NOT NULL, goals TEXT, status TEXT NOT NULL DEFAULT 'pending', stripe_session_id TEXT, meeting_link TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (slot_id) REFERENCES slots(id) ); `); // --------------------------------------------------------------------------- // Email (Gmail SMTP using an App Password — see README) // --------------------------------------------------------------------------- const transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_APP_PASSWORD } }); function sendMail(to, subject, html) { return transporter.sendMail({ from: `"Sprout & Sum Tutoring" <${process.env.EMAIL_USER}>`, to, subject, html }); } // --------------------------------------------------------------------------- // Stripe webhook — MUST use the raw body, so this route is registered // before express.json() is applied globally. // --------------------------------------------------------------------------- app.post('/api/stripe-webhook', express.raw({ type: 'application/json' }), async (req, res) => { let event; try { event = stripe.webhooks.constructEvent( req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'checkout.session.completed') { const session = event.data.object; const bookingId = session.metadata.booking_id; const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(bookingId); if (booking && booking.status !== 'paid') { const meetingLink = process.env.DEFAULT_MEETING_LINK || 'https://meet.google.com/your-link'; db.prepare(`UPDATE bookings SET status = 'paid', meeting_link = ? WHERE id = ?`) .run(meetingLink, bookingId); if (booking.slot_id) { db.prepare('UPDATE slots SET booked = 1 WHERE id = ?').run(booking.slot_id); } const slot = booking.slot_id ? db.prepare('SELECT * FROM slots WHERE id = ?').get(booking.slot_id) : null; const whenText = slot ? new Date(slot.start_time_utc).toUTCString() : 'the time you requested'; try { await sendMail( booking.parent_email, 'Your tutoring session is confirmed!', `

Hi ${booking.parent_name},

Your ${booking.subject} session for ${booking.child_name} is confirmed for ${whenText} (UTC).

Join here when it's time: ${meetingLink}

See you soon!
Sprout & Sum Tutoring

` ); await sendMail( process.env.EMAIL_USER, 'New paid booking', `

${booking.parent_name} (${booking.parent_email}) booked a ${booking.subject} session for ${booking.child_name}, age ${booking.child_age}.

Time: ${whenText} (UTC)

Goals/concerns: ${booking.goals || 'none provided'}

` ); } catch (mailErr) { console.error('Email send failed:', mailErr); } } } res.json({ received: true }); }); // --------------------------------------------------------------------------- // Normal middleware for every other route // --------------------------------------------------------------------------- app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // --------------------------------------------------------------------------- // GET /api/slots — list open, unbooked slots // --------------------------------------------------------------------------- app.get('/api/slots', (req, res) => { const slots = db.prepare( 'SELECT id, start_time_utc FROM slots WHERE booked = 0 AND start_time_utc > ? ORDER BY start_time_utc ASC' ).all(new Date().toISOString()); res.json(slots); }); // --------------------------------------------------------------------------- // POST /api/admin/slots — add a new available slot (protected by ADMIN_KEY) // Body: { "start_time_utc": "2026-09-02T21:00:00.000Z" } // --------------------------------------------------------------------------- app.post('/api/admin/slots', (req, res) => { if (req.headers['x-admin-key'] !== process.env.ADMIN_KEY) { return res.status(401).json({ error: 'Unauthorized' }); } const { start_time_utc } = req.body; if (!start_time_utc) return res.status(400).json({ error: 'start_time_utc is required' }); const info = db.prepare('INSERT INTO slots (start_time_utc) VALUES (?)').run(start_time_utc); res.json({ id: info.lastInsertRowid }); }); // --------------------------------------------------------------------------- // GET /api/admin/bookings — view all bookings (protected by ADMIN_KEY) // --------------------------------------------------------------------------- app.get('/api/admin/bookings', (req, res) => { if (req.headers['x-admin-key'] !== process.env.ADMIN_KEY) { return res.status(401).json({ error: 'Unauthorized' }); } const bookings = db.prepare('SELECT * FROM bookings ORDER BY created_at DESC').all(); res.json(bookings); }); // --------------------------------------------------------------------------- // POST /api/bookings — create a booking, then start a Stripe Checkout session // --------------------------------------------------------------------------- app.post('/api/bookings', async (req, res) => { try { const { slot_id, parent_name, parent_email, child_name, child_age, subject, goals } = req.body; if (!parent_name || !parent_email || !child_name || !child_age || !subject) { return res.status(400).json({ error: 'Please fill in all required fields.' }); } if (slot_id) { const slot = db.prepare('SELECT * FROM slots WHERE id = ? AND booked = 0').get(slot_id); if (!slot) return res.status(409).json({ error: 'That time was just booked by someone else — please pick another.' }); } const info = db.prepare(` INSERT INTO bookings (slot_id, parent_name, parent_email, child_name, child_age, subject, goals) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(slot_id || null, parent_name, parent_email, child_name, child_age, subject, goals || ''); const bookingId = info.lastInsertRowid; const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], mode: 'payment', customer_email: parent_email, line_items: [{ price_data: { currency: 'usd', product_data: { name: `1-hour ${subject} session for ${child_name}` }, unit_amount: SESSION_PRICE_CENTS }, quantity: 1 }], metadata: { booking_id: String(bookingId) }, success_url: `${process.env.SITE_URL}/booking-success.html?booking_id=${bookingId}`, cancel_url: `${process.env.SITE_URL}/#book` }); db.prepare('UPDATE bookings SET stripe_session_id = ? WHERE id = ?').run(session.id, bookingId); res.json({ checkout_url: session.url }); } catch (err) { console.error(err); res.status(500).json({ error: 'Something went wrong creating your booking. Please try again.' }); } }); // --------------------------------------------------------------------------- // POST /api/contact — simple contact form // --------------------------------------------------------------------------- app.post('/api/contact', async (req, res) => { try { const { name, email, message } = req.body; if (!name || !email || !message) return res.status(400).json({ error: 'Missing fields' }); await sendMail( process.env.EMAIL_USER, `Website message from ${name}`, `

From: ${name} (${email})

${message}

` ); res.json({ ok: true }); } catch (err) { console.error(err); res.status(500).json({ error: 'Could not send your message. Please email us directly.' }); } }); app.listen(PORT, () => console.log(`Sprout & Sum server running on port ${PORT}`)); Booking Confirmed — Sprout & Sum Tutoring

Payment received 🎉

Thank you! Your session is booked. A confirmation email with your lesson link is on its way to your inbox.

If it doesn't arrive within a few minutes, check spam or email eshiphangitu@gmail.com.

Back to homepage