<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fundsroom Tech</title>
    <description>The latest articles on DEV Community by Fundsroom Tech (@fundsroom_tech_82070fdbc3).</description>
    <link>https://dev.to/fundsroom_tech_82070fdbc3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4154648%2F81265c40-4346-41ac-8f24-b855269237a3.png</url>
      <title>DEV Community: Fundsroom Tech</title>
      <link>https://dev.to/fundsroom_tech_82070fdbc3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fundsroom_tech_82070fdbc3"/>
    <language>en</language>
    <item>
      <title>Running a Multi-Tenant Platform on EC2: Node.js, PostgreSQL, SES and the AWS Cost Bug We Almost Missed</title>
      <dc:creator>Fundsroom Tech</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:54:30 +0000</pubDate>
      <link>https://dev.to/fundsroom_tech_82070fdbc3/running-a-multi-tenant-platform-on-ec2-nodejs-postgresql-ses-and-the-aws-cost-bug-we-almost-jgo</link>
      <guid>https://dev.to/fundsroom_tech_82070fdbc3/running-a-multi-tenant-platform-on-ec2-nodejs-postgresql-ses-and-the-aws-cost-bug-we-almost-jgo</guid>
      <description>&lt;p&gt;description: Real architecture notes from FundsWeb and FundsAudit: Express, Sequelize, PM2, Nginx, SES, S3 and a cross-region data transfer bill that taught us to read AWS cost reports.&lt;br&gt;
tags: aws, node, postgres, architecture&lt;br&gt;
canonical_url: &lt;a href="https://fundsroomprojects.com/blog/multi-tenant-platform-ec2-node-postgres" rel="noopener noreferrer"&gt;https://fundsroomprojects.com/blog/multi-tenant-platform-ec2-node-postgres&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  cover_image:
&lt;/h2&gt;

&lt;p&gt;We build and run &lt;a href="https://fundsweb.in" rel="noopener noreferrer"&gt;FundsWeb&lt;/a&gt;, a multi-tenant web platform, along with [FundsAudit] and an internal email validation service. This post covers the stack, the decisions we'd make again, and one AWS billing issue that cost us more than it should have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime and API&lt;/td&gt;
&lt;td&gt;Node.js with Express&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL, installed directly on EC2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORM&lt;/td&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process manager&lt;/td&gt;
&lt;td&gt;PM2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse proxy&lt;/td&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;AWS SES&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;Route 53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF and document generation&lt;/td&gt;
&lt;td&gt;Puppeteer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is deliberately plain. For a small team, fewer moving parts means faster debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why EC2 with PM2 and Nginx
&lt;/h2&gt;

&lt;p&gt;We run the app on EC2 instead of a managed container platform.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PM2&lt;/strong&gt; restarts crashed processes, manages logs and gives us zero-downtime reloads with &lt;code&gt;pm2 reload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt; terminates TLS, serves static files, and proxies to the Node process. Each tenant domain is a server block pointing to the same app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off:&lt;/strong&gt; we own patching, backups and scaling. Managed services remove that work. We chose control and predictable cost, and we accept the operational load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Tenancy with Sequelize
&lt;/h2&gt;

&lt;p&gt;Tenant context is resolved once per request and passed to the data layer, not rebuilt inside each controller.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Models include a tenant identifier, and scoped queries apply it centrally.&lt;/li&gt;
&lt;li&gt;Sequelize scopes keep the filter in one place, so a developer can't forget it on a single route.&lt;/li&gt;
&lt;li&gt;Migrations are versioned and run per environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;[Add one real detail: how you resolve the tenant (subdomain, domain, token) and one bug this design prevented.]&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generating documents with Puppeteer
&lt;/h2&gt;

&lt;p&gt;We use Puppeteer to render HTML templates into PDFs for [reports / audit documents, edit to match].&lt;/p&gt;

&lt;p&gt;What we learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat the browser like any other heavy dependency: limit concurrency, set timeouts, and always close pages.&lt;/li&gt;
&lt;li&gt;Render from the same HTML templates the UI uses, so layout stays consistent.&lt;/li&gt;
&lt;li&gt;Memory spikes appear when many PDFs are queued at once, so queue them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Email on SES, and building FundsMailer
&lt;/h2&gt;

&lt;p&gt;Sending email through SES is easy. Keeping your sender reputation healthy is the hard part. Bounces and complaints affect delivery, so we built &lt;strong&gt;FundsMailer&lt;/strong&gt;, an internal email validation service.&lt;/p&gt;

&lt;p&gt;It checks addresses before sending, runs a set of validation checks, produces a score, and exposes the result through an API.&lt;/p&gt;

&lt;p&gt;One limitation worth knowing: probing mail servers over SMTP from EC2 has constraints, because AWS restricts outbound port 25 by default. So SMTP-level checks can't be the only signal. [Add which checks you rely on instead: syntax, DNS/MX, disposable-domain lists, etc.]&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The cross-region bill
&lt;/h2&gt;

&lt;p&gt;This was the most useful lesson.&lt;/p&gt;

&lt;p&gt;Our EC2 instance and SES were in &lt;code&gt;us-east-1&lt;/code&gt;, but the S3 buckets used by the email pipeline were in &lt;code&gt;ap-south-1&lt;/code&gt;. Every read and write crossed regions, and AWS charges for inter-region data transfer. The cost showed up in billing before it showed up in any monitoring.&lt;/p&gt;

&lt;p&gt;What we did:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Used Cost Explorer, grouped by usage type, to find the data transfer line items.&lt;/li&gt;
&lt;li&gt;Traced them to the services moving data between regions.&lt;/li&gt;
&lt;li&gt;Created a new bucket in &lt;code&gt;us-east-1&lt;/code&gt;, next to SES and EC2, and migrated the pipeline to it.&lt;/li&gt;
&lt;li&gt;Repeated the same setup in our second AWS account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[Add the real before/after cost figure if you're comfortable sharing it, or say "a significant share of the bill".]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule we follow now:&lt;/strong&gt; keep compute, email and storage in the same region unless there's a reason not to, and check the cost report by usage type every month.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Don't leave a bucket public by accident
&lt;/h2&gt;

&lt;p&gt;While auditing, we found a bucket serving images that was publicly writable or listable more than it needed to be. [Edit to match what you actually found.] We fixed it by turning on Block Public Access and serving public images through a controlled path.&lt;/p&gt;

&lt;p&gt;Review your bucket policies and ACLs on a schedule. It takes ten minutes and prevents long incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. FundsAudit: what's different
&lt;/h2&gt;

&lt;p&gt;[Write 4-6 real lines: what FundsAudit does, which part of the stack it leans on (for example Puppeteer reports, role-based access, audit trails), and one design decision you'd defend.]&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell a team starting today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep the stack boring until a real constraint forces a change.&lt;/li&gt;
&lt;li&gt;Resolve tenant context in one place.&lt;/li&gt;
&lt;li&gt;Queue anything heavy, like PDFs or email sends.&lt;/li&gt;
&lt;li&gt;Keep compute, storage and email in one region.&lt;/li&gt;
&lt;li&gt;Read the AWS cost report by usage type every month.&lt;/li&gt;
&lt;li&gt;Audit bucket policies regularly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We build custom ERP and SaaS products at &lt;a href="https://fundsroomprojects.com" rel="noopener noreferrer"&gt;Fundsroom&lt;/a&gt;. If you're solving similar problems, tell us in the comments what you'd do differently.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>node</category>
      <category>postgres</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
