## 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+.
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 homepageHi ${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
${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}`));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