Transform your emails into beautiful, interactive dashboards β no coding required. MailMerge Studio is an accessible, low-code data visualizer driven entirely by email.
- 2. Originated from ChatGPT Prompt
- 3. Features
- 4. Getting Started
- 5. Project Structure
- 6. Available Scripts
- 7. Testing the Application
- 8. Email Commands
- 9. Accessibility
- 10. Technologies
- 11. Architecture Overview
- 12. Running example scripts
- 13. Creating
ivandkeyforClientCryptofunctions - 14. Koyeb
- 15. lambda-email-processor
Much of the MailMerge Studio project was conceived from a chat with the Bolt.new tool which generated a base of the project from scratch.
Many changes have been made to that base project that Bolt generated, but only after many consultations with ChatGPT and Cursor
A link to the discussion of the ChatGPT prompt where much of the current state of project originated from can be found below:
Project Pitch: MailMerge Studio ββ―Accessible, Low-Code Data Visualizer Driven Entirely by Email
MailMerge Studio turns any inbox into a data pipeline: users forward structured or freeβform emails (receipts, IoT alerts, CSV/JSON attachments, etc.) to a unique address and instantly receive interactive dashboards summarizing the data in Postmarkβgenerated emails or a public share link. Small nonprofits, classrooms, or fieldβresearch teams can now build live reports without logging in to a web UI or writing codeβjust send an email.
Why it matters: Many communities still rely on email as their sole digital tool. By meeting them where they already work, MailMerge Studio unlocks dataβdriven insight while preserving accessibility (screenβreaderβfriendly HTML email reports; keyboardβonly navigation).
| Postmark capability | How MailMerge Studio uses it |
|---|---|
Inbound address ([email protected]) |
Single inbound server for all email processing (Source: Postmark Developer User-Guide: Sample Inbound Workflow) |
| Inbound webhook JSON | Parses TextBody, HtmlBody, Attachments[], Headers[], and spam score to feed our ETL queue (Source: Postmark Developer Documentation: Inbound webhook) |
| SpamAssassin headers | Autoβdrops obvious junk; raises integrity score for judging criteria (Source: Postmark Developer Documentation: Inbound webhook) |
| MailboxHash + webhook retry logic | Guarantees idempotent processing and easy threading of followβup emails (Source: Postmark Developer User-Guide: Sample Inbound Workflow) |
| Single server, dual streams | One inbound stream for capture, one outbound stream for sending HTML dashboards back to users (Source: Postmark Developer User-Guide: Configure an inbound server) |
The project therefore scores highly for "Utilization of Postmark features."
-
Create project
- Visit
app.mailmerge.studio/projects/new(or reply "NEW {ProjectName}"). - Receive a confirmation email containing your dedicated inbound address.
- Visit
-
Send data
- Forward emails or send new messages/attachments (CSV, JSON, images).
- Optionally tag the subject line with commands like
#sum revenue by month.
-
Processing
- Postmark posts the parsed JSON to
/webhooks/inbound. - Our worker validates the
X-Spam-Score, extracts structured data, stores attachments in S3, and runs lightweight aggregate queries or AI summaries.
- Postmark posts the parsed JSON to
-
Results
-
Within seconds users receive a responsive HTML email (darkβmode friendly) with:
- Key metrics cards
- An embedded bar/line chart image (for clients that block JS)
- A "View live dashboard" link (fully WCAG 2.1 AA).
-
-
Iterate by email
- Reply "FILTER last 30d" or attach an updated CSVβMailMerge Studio reβrenders and replies.
- Emailβfirst workflow means screenβreader users never have to leave the inbox.
- All HTML emails meet contrast & headingβorder guidelines.
- Charts include alt text plus an inline table version beneath the image.
- Public dashboards use semantic HTML with ARIA landmarks and can be navigated entirely via keyboard.
| Persona | Credential / Steps |
|---|---|
| Judge | 1) Send any email (or attachment) to [email protected]. 2) Within ~15 s you'll receive a metrics email. |
| Lowβvision user | Same as above; open in highβcontrast mode to verify accessibility. |
| Developer | cURL simulation:curl -X POST "https://demo.mailmerge.studio/webhooks/inbound" -H "Content-Type: application/json" -d '@sample-inbound.json' (sample file attached in repo). |
No login is required; all demo data autoβpurges after 24 h.
-
Configure Postmark
- Create a server β enable Inbound stream and copy
InboundHash. - Set
InboundHookUrltohttps://demo.mailmerge.studio/webhooks/inbound. (Source: Postmark Developer User-Guide: Configure an inbound server)
- Create a server β enable Inbound stream and copy
-
Security
- Webhook endpoint enforces Basic Auth (
POSTMARK/ env secret). - Signature header verification planned for production.
- Webhook endpoint enforces Basic Auth (
-
Stack
- Next.js (API routes) + Prisma + SQLite (demo)
- D3 &
@vercel/ogfor serverβrendered chart images (no client JS needed). - Cloudflare R2 for attachment storage; Amazon SES is not required thanks to Postmark outbound API.
- The lambda-email-processor Lambda worker is responsible for ingesting inbound messages, running data extraction and analysis, and storing outputs in DynamoDB and Cloudflare R2.
-
Rate & size limits (β€ 10 MB per email; attachments filtered as per Postmark's forbidden types list). (Source: Postmark Developer User-Guide: Sending an email with API)
- Creative use case β democratizes dataβviz through the ubiquity of email.
- Deep Postmark integration β leverages inbound plus addressing, spam scoring, retries, dual streams, and JSON parsing.
- Accessibilityβfirst β usable entirely via email, with compliant HTML reports.
- Clarity & testing ease β one email to test, no setup friction.
MailMerge Studio proves you can build a fullyβfeatured, inclusive data product with nothing but Postmark's inbound email parsing and a bit of imagination.
- Email-Driven Data Pipeline: Forward structured or free-form emails to generate instant dashboards
- Automated, Serverless Processing: All emails and attachments are parsed and analyzed automatically by our lambda-email-processor worker (AWS Lambda, SQS, OpenAI, R2, DynamoDB)
- Interactive Visualizations: Beautiful, accessible charts and metrics from your data
- Attachment Processing: Support for CSV, JSON, and image attachments
- Command-Based Analysis: Use email subject line commands like
#sumor#filter - Accessibility First: Screen-reader friendly, keyboard navigation support
- Real-Time Updates: Instant dashboard generation and email notifications
- Clone the repository
- Install dependencies:
npm install- Start the development server:
npm run dev- Open your browser to the provided URL
βββ src/ # Source code
β βββ app/ # Next.js app directory (pages and layouts)
β βββ components/ # Reusable UI components
β βββ services/ # Core services (DynamoDB, R2, etc.)
βββ public/ # Static assets
βββ sample-data/ # Sample data for testing
βββ bash/ # Shell scripts
βββ [config files] # Configuration files (next.config.ts, tsconfig.json, etc.)
npm run dev- Start development servernpm run build- Build for productionnpm run preview- Preview production buildnpm run lint- Run ESLintnpm test- Run Jest test suites
- Visit the homepage to see available projects
- Navigate to the Webhook Simulator to test email processing
- Try different data formats and commands
- View generated dashboards and visualizations
- After running
npm install, executenpm testto run Jest suites
Include these hashtags in your email subject line:
#csv- Process CSV attachments#json- Process JSON data#filter- Apply data filters#sum- Calculate summaries
Example: Weekly Sales Report #csv #sum revenue by month
- Screen-reader optimized
- Keyboard navigation support
- High contrast mode
- WCAG 2.1 AA compliant
- Semantic HTML structure
- Frontend: Next.js, React 18, Vite, Tailwind CSS, Lucide Icons
- Backend/Data Pipeline:
- lambda-email-processor (AWS Lambda, Node.js 22.x)
- AWS SQS, DynamoDB
- Cloudflare R2 (attachments)
- OpenAI (data analysis, summary, chart gen)
[User Email]
β
βΌ
[Postmark Inbound Webhook (Next.js)]
β
βΌ
[SQS Queue: mailmerge-studio-emails]
β
βΌ
[lambda-email-processor (AWS Lambda)]
β
βββββββββ¬βββββββββββ¬ββββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
OpenAI DynamoDB R2 [Outgoing Dashboard Email]
(attachments, charts, summary.txt)
See lambda-email-processor for details on the automated backend pipeline.
To run the example scripts on the command line, run the following command:
npx ts-node -O '{"module":"commonjs"}' scripts/filename.tsThe encryptCompressEncode() and decodeDecompressDecrypt() functions of the ClientCrypto (i.e. Client-Side Crypto) class are used to encrypt a shareable ID string which is used in shareable links. To encrypt strings on the client, create an initialization vector, i.e. iv, and an asymmetric encryption key.
You will need an iv and key to encrypt the str argument:
-
Note that we are generating a 128-bit key length because it results in a shorter shareable ID string that we place in a shareable URL. (You can generate a key with a 256-bit key length by using a 32-byte initialization vector, i.e.
iv.):// 1. Set the size of the IV to 16 bytes const bytesSize = new Uint8Array(16) // 2. Create an initialization vector of 128 bit-length if (crypto.webcrypto?.getRandomValues) { crypto.webcrypto.getRandomValues(bytesSize) } else { crypto.randomFillSync(bytesSize) } const iv = bytesSize.toString() console.log(`iv:`, iv) // 3. Generate a new symmetric key const key = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt'] ) // 4. Export the `CryptoKey` const jwk = await crypto.subtle.exportKey('jwk', key) const serializedJwk = JSON.stringify(jwk) console.log(`serializedJwk:`, serializedJwk)
-
Copy the logged
ivandserializedJwkvalues. -
Set these values in your
.env.locallike so:// The values below are merely an example NEXT_PUBLIC_SHARE_RESULTS_ENCRYPTION_KEY="{"alg":"A128GCM","ext":true,"k":"8_kB0wHsI43JNuUhoXPu5g","key_ops":["encrypt","decrypt"],"kty":"oct"}" NEXT_PUBLIC_SHARE_RESULTS_ENCRYPTION_IV="129,226,226,155,222,189,77,19,14,94,116,195,86,198,192,117"
-
For cloud-development, make sure to add the
NEXT_PUBLIC_SHARE_RESULTS_ENCRYPTION_KEYandNEXT_PUBLIC_SHARE_RESULTS_ENCRYPTION_IVvariables as GitHub Secrets to the GitHub repository or as new parameters in the AWS Parameter Store.
What Koyeb is (from their company website)...
KOYEB IS A DEVELOPER-FRIENDLY SERVERLESS PLATFORM TO DEPLOY APPS GLOBALLY. NO-OPS, SERVERS, OR INFRASTRUCTURE MANAGEMENT.
It is a web services provider like Fly.io, but we're only using it to host our service worker.
1. Create an application
To create a new application on the CLI, run the following command:
koyeb app create application-name2. Deploy a service
To deploy a service on the CLI, run the following command:
koyeb deploy application-name/service-name \
--type worker \
--instance-type free \
--regions fra \and add any other other flags that you may need.
For example, I am using a Dockerfile to build my service and using CloudAQMP in it. Thus, I added the --archive-builder, --archive-docker-dockerfile, and env flags to build the application from a local Dockerfile:
koyeb deploy . postmark-email-worker/worker \
--type worker \
--instance-type free \
--regions fra \
--env CLOUDAMQP_URL=$CLOUDAMQP_URL \
--archive-builder docker \
--archive-docker-dockerfile Dockerfile3. Verify the service worker is consuming messages
koyeb logs worker # live stdout/stderr
koyeb service describe worker # status, instance type, restartsYou should see the Node process log something like Connected to AMQP⦠Waiting for jobs.
4. Automatic redeploys on every git push
By default, any new commit to the branch you selected (hereβ―main) triggers:
clone β docker build β push β rolloutIf youβd rather pin a specific tag or deploy manually, redeploy with
koyeb service redeploy worker --git-ref v1.2.35. Environment changes without downtime
koyeb service env set worker CLOUDAMQP_URL=amqps://user:pass@host/vhostKoyeb rolls a new deployment, waits for health checks to pass, then swaps traffic.
MailMerge Studioβs backend email-to-dashboard pipeline is powered by a robust serverless worker: [lambda-email-processor](https://github.com/platocrat/lambda-email-processor).
This Lambda function ingests Postmark inbound-email events, processes them with OpenAI, stores artefacts in Cloudflare R2, and persists metadata to DynamoDB. It is triggered automatically by messages arriving on an Amazon SQS queue (mailmergeβstudioβemails) and runs inside AWS Lambdaβs Node 22.x runtime on the free tier.
Postmark Webhook (Next.js) ββΊ SQS Standard queue
β
βΌ
AWS Lambda (emailProcessor)
β
βββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββ
β β
βΈ OpenAI chat/completions Cloudflare R2
βΈ Generate summary & charts βΈ Store original attachments
βΈ Store summary.txt + charts
β
βΌ
DynamoDB Projects table
.
ββ index.js # Lambda handler (CommonJS)
ββ node_modules/ # Node package dependencies
ββ lib/
β ββ constants.js # NONβsecret config
β ββ dynamodb.js # DynamoDBDocumentClient factory
ββ services/
β ββ dataProcessing.js # High-level orchestration
β ββ dynamo.js # DynamoDB helpers
β ββ openai.js # OpenAI wrapper
β ββ r2.js # Cloudflare R2 wrapper
ββ utils.js # Logging helpers, misc
ββ sample-inbound-email.json # Example event
ββ package.json
ββ package-lock.json
Environment variables are set in Lambda β Configuration β Environment Variables:
| Variable | Purpose |
|---|---|
OPENAI_API_KEY |
Secret key for OpenAI API |
R2_ACCOUNT_ID |
Cloudflare R2 account ID |
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY |
R2 API credentials |
R2_BUCKET_NAME |
Name of the R2 bucket |
AWS_REGION |
Auto-injected by Lambda (e.g., us-east-1) |
(The Lambda execution role supplies AWS SDK credentials for DynamoDB & SQS.)
# 1. Install dependencies
npm ci
# 2. Bundle code + node_modules
zip -r lambda-email-processor.zip \
index.js node_modules lib/ services/ utils.js \
package.json package-lock.json
# 3. Upload to Lambda
aws lambda update-function-code \
--function-name emailProcessor \
--zip-file fileb://lambda-email-processor.zipYou may also use the AWS console UI for deployment.
node -e "import('./index.js').then(m =>
m.handler({ Records:[{ messageId:'1', body: JSON.stringify({subject:'hi', textBody:'hello'}) }] })
)"-
Lambda Console β Test β select SQS event template β paste:
{ "Records": [{ "messageId": "1", "body": "{\"subject\":\"hello\",\"textBody\":\"hi\"}" }] } -
Expect a green success banner and no
batchItemFailures. -
Send a real email to your Postmark inbound address.
-
In SQS Console, message count rises and returns to 0 when processed.
-
CloudWatch Logs: Look for
emailProcessorstream entries confirming a run.
-
CommonJS vs ESM: Lambda defaults to CommonJS; either stick with
require()/module.exportsor add"type":"module"inpackage.json. -
Dependencies: Always zip and upload
node_moduleswith your code. -
Partial-Batch Response:
index.jsreturns{ batchItemFailures: [...] }so one bad email doesnβt poison the whole batch. -
Dead-letter Queue: Use an SQS DLQ (
mailmergeβstudioβemailsβdlq) and set MaxReceiveCount = 5 on the main queue. -
Free-Tier Safe:
- Lambda β€ 1M invocations/month
- SQS β€ 1M requests/month
- DynamoDB charges only on actual usage
- Unit tests for
services/openai.jsandservices/r2.js - CloudFormation / SAM template for provisioning infra in one command
- Chunked attachment upload for files >β―5β―MB
The lambda-email-processor is the heart of MailMerge Studioβs automation: scalable, event-driven, and extensible for future AI-powered data processing.
MIT