// 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