Skip to content

Commit dd06e43

Browse files
committed
Store contact submissions in a KV namespace
Bind a SUBMISSIONS KV namespace and persist every valid submission as a durable record (keyed submission:<ts>:<uuid>, with email/project/topic/date in KV metadata). Records are written before email is sent, so a lead survives even if delivery fails. Email is no longer the only copy.
1 parent 8c106b1 commit dd06e43

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎workers/contact-form/README.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,27 @@ transactional emails through MailLayer (`mail.simplebytes.com`):
1010
2. **Admin notification** to `ADMIN_EMAIL` (`hello@simplebytes.com`), with
1111
`reply-to` set to the submitter so you can reply directly.
1212

13+
Every valid submission is also stored in the `SUBMISSIONS` KV namespace as a durable
14+
record (email is no longer the only copy).
15+
1316
The source here was recovered from the live deployment; keep it as the source of truth.
1417

18+
## Reading stored entries
19+
20+
Submissions are written under keys like `submission:<timestamp>:<uuid>`, with the
21+
email/project/topic/date mirrored into KV metadata for quick scanning:
22+
23+
```sh
24+
# List recent entries (key + metadata, no full value)
25+
wrangler kv key list --binding SUBMISSIONS
26+
27+
# Read one full entry
28+
wrangler kv key get --binding SUBMISSIONS "submission:1720000000000:...."
29+
```
30+
31+
Records are stored before the emails are sent, so a lead is preserved even if
32+
email delivery fails.
33+
1534
## Request shape
1635

1736
`POST` JSON: `{ name, email, project, topic, message, website, turnstileToken }`

‎workers/contact-form/src/index.js‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ export default {
8484
const projectLabel = PROJECT_LABELS[project] || project;
8585
const topicLabel = TOPIC_LABELS[topic] || topic;
8686

87+
// Persist a durable record before sending, so a lead is never lost even
88+
// if email delivery fails. Storage problems must not break the response.
89+
await storeSubmission(env, request, {
90+
name,
91+
email,
92+
project,
93+
projectLabel,
94+
topic,
95+
topicLabel,
96+
message,
97+
});
98+
8799
// Confirmation to the submitter.
88100
await sendEmail(env, {
89101
to: email,
@@ -118,6 +130,33 @@ function isValidEmail(email) {
118130
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
119131
}
120132

133+
async function storeSubmission(env, request, fields) {
134+
if (!env.SUBMISSIONS) return;
135+
try {
136+
const submittedAt = new Date().toISOString();
137+
// Sortable key: newest entries sort last by timestamp, unique by UUID.
138+
const key = `submission:${Date.now()}:${crypto.randomUUID()}`;
139+
const record = {
140+
...fields,
141+
submittedAt,
142+
ip: request.headers.get('CF-Connecting-IP'),
143+
country: request.cf && request.cf.country,
144+
userAgent: request.headers.get('User-Agent'),
145+
};
146+
await env.SUBMISSIONS.put(key, JSON.stringify(record), {
147+
// Lightweight fields so `kv key list` is useful without reading each value.
148+
metadata: {
149+
email: fields.email,
150+
project: fields.projectLabel,
151+
topic: fields.topicLabel,
152+
submittedAt,
153+
},
154+
});
155+
} catch (error) {
156+
console.error('Failed to store submission:', error);
157+
}
158+
}
159+
121160
async function verifyTurnstile(token, secretKey) {
122161
if (!token || !secretKey) return false;
123162
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {

‎workers/contact-form/wrangler.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ compatibility_date = "2024-01-01"
77
ADMIN_EMAIL = "hello@simplebytes.com"
88
FROM_EMAIL = "noreply@simplebytes.com"
99

10+
# Stores every valid submission as a durable record (see src/index.js).
11+
[[kv_namespaces]]
12+
binding = "SUBMISSIONS"
13+
id = "1ef74d0d5a994477848a453437032a46"
14+
1015
# Secrets — set once with `wrangler secret put <NAME>`, never committed:
1116
# TURNSTILE_SECRET_KEY Cloudflare Turnstile secret key
1217
# ADMIN_TEMPLATE_ID MailLayer transactional key for the admin notification email

0 commit comments

Comments
 (0)