// Waitlist API endpoint using PostgreSQL // Simple interest signups with email confirmation via Mailcow SMTP const { Pool } = require('pg'); const nodemailer = require('nodemailer'); const { syncWaitlistSignup } = require('./google-sheets'); const { addToListmonk } = require('./listmonk'); // Initialize PostgreSQL connection pool const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: false }); // Initialize SMTP transport (Mailcow) const smtp = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'mail.rmail.online', port: parseInt(process.env.SMTP_PORT || '587'), secure: false, auth: { user: process.env.SMTP_USER || 'contact@valleyofthecommons.com', pass: process.env.SMTP_PASS || '', }, tls: { rejectUnauthorized: false }, }); const welcomeEmail = (signup) => ({ subject: 'Welcome to the Valley — A Village Built on Common Ground', html: `

Welcome to the Valley!

A village built on common ground

Dear ${signup.name},

Thank you for stepping toward something different. Valley of the Commons is a four-week pop-up village in Austria's Höllental Valley (August 24 – September 20, 2026) — a living commons shared in work and study, in making and care, in governance and everyday life.

For four weeks, we'll come together to lay the foundations for life beyond extractive systems. Each week explores a different dimension of what a commons-based society can look like:

Week 1 Return of the Commons
Week 2 Cosmo-local Production & Open Value Accounting
Week 3 Future Living
Week 4 Governance & Funding

Mornings are structured learning paths. Afternoons host workshops, field visits, and working groups. And in between — shared meals, hikes into the Alps, river swimming, mushroom foraging, fire circles, and the kind of conversations that only happen when people live and build together.

${signup.involvement ? `
What you're bringing:

${signup.involvement}

` : ''}

We'll be in touch with application details, event updates, and ways to get involved as the village takes shape.

Apply Now

See you in the valley,
The Valley of the Commons Team


You received this email because you signed up at valleyofthecommons.com.
Unsubscribe

` }); async function logEmail(recipientEmail, recipientName, emailType, subject, messageId, metadata = {}) { try { await pool.query( `INSERT INTO email_log (recipient_email, recipient_name, email_type, subject, message_id, metadata) VALUES ($1, $2, $3, $4, $5, $6)`, [recipientEmail, recipientName, emailType, subject, messageId, JSON.stringify(metadata)] ); } catch (error) { console.error('Failed to log email:', error); } } module.exports = async function handler(req, res) { // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.status(200).end(); } if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const { email, name, involvement } = req.body; // Validate email if (!email || !email.includes('@')) { return res.status(400).json({ error: 'Valid email is required' }); } // Validate name if (!name || name.trim() === '') { return res.status(400).json({ error: 'Name is required' }); } // Validate involvement if (!involvement || involvement.trim() === '') { return res.status(400).json({ error: 'Please describe your desired involvement' }); } const emailLower = email.toLowerCase().trim(); const nameTrimmed = name.trim(); const involvementTrimmed = involvement.trim(); // Check if email already exists const existing = await pool.query( 'SELECT id FROM waitlist WHERE email = $1', [emailLower] ); if (existing.rows.length > 0) { // Update existing entry await pool.query( 'UPDATE waitlist SET name = $1, involvement = $2 WHERE email = $3', [nameTrimmed, involvementTrimmed, emailLower] ); return res.status(200).json({ success: true, message: 'Your information has been updated!' }); } // Insert new signup const result = await pool.query( `INSERT INTO waitlist (email, name, involvement) VALUES ($1, $2, $3) RETURNING id`, [emailLower, nameTrimmed, involvementTrimmed] ); const signup = { id: result.rows[0].id, email: emailLower, name: nameTrimmed, involvement: involvementTrimmed }; // Sync to Google Sheets (fire-and-forget backup) syncWaitlistSignup(signup); // Add to Listmonk newsletter addToListmonk(signup.email, signup.name, { involvement: signup.involvement, source: 'waitlist', }).catch(err => console.error('[Listmonk] Waitlist sync failed:', err.message)); // Send welcome email if (process.env.SMTP_PASS) { try { const email = welcomeEmail(signup); const info = await smtp.sendMail({ from: process.env.EMAIL_FROM || 'Valley of the Commons ', to: signup.email, subject: email.subject, html: email.html, }); await logEmail(signup.email, signup.name, 'waitlist_welcome', email.subject, info.messageId); } catch (emailError) { console.error('Failed to send welcome email:', emailError); // Don't fail the request if email fails } } return res.status(200).json({ success: true, message: 'Successfully joined the waitlist!' }); } catch (error) { console.error('Waitlist error:', error); return res.status(500).json({ error: 'Failed to join waitlist. Please try again later.' }); } };