<?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: Joyce</title>
    <description>The latest articles on DEV Community by Joyce (@jwambui).</description>
    <link>https://dev.to/jwambui</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%2F1257370%2Febe95777-665e-43c7-9bfb-bbb747d89f17.JPG</url>
      <title>DEV Community: Joyce</title>
      <link>https://dev.to/jwambui</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jwambui"/>
    <language>en</language>
    <item>
      <title>Modeling a Creator SaaS in a Single DynamoDB Table</title>
      <dc:creator>Joyce</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:06:43 +0000</pubDate>
      <link>https://dev.to/jwambui/exploring-single-h38</link>
      <guid>https://dev.to/jwambui/exploring-single-h38</guid>
      <description>&lt;p&gt;&lt;em&gt;Submitted to the AWS H0 Hackathon — Vercel v0 + AWS Databases track. #H0Hackathon&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Truss is a B2C SaaS for content creators. You upload a video or connect a live stream, and Truss uses Gemini to extract the highest-engagement moments, scores each for virality, and publishes 9:16 vertical clips across YouTube, TikTok, Twitch, and Discord.&lt;/p&gt;

&lt;p&gt;The data model behind this sounds deceptively simple: users, videos, clips, streams, analytics, platform tokens. Six entities. But the &lt;em&gt;access patterns&lt;/em&gt; are what matter — and they pushed me toward a single-table DynamoDB design over a relational database in ways I didn't fully anticipate when I started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why DynamoDB Instead of Postgres
&lt;/h2&gt;

&lt;p&gt;The first question I get is always: why not just use a relational database?&lt;/p&gt;

&lt;p&gt;Three reasons shaped this decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The dominant read pattern is partition-scoped.&lt;/strong&gt; Almost every page in this app asks the same question: &lt;em&gt;"Give me everything for user X, filtered by entity type."&lt;/em&gt; Dashboard needs recent assets and analytics. Clips page needs clips. Streams page needs streams. In SQL, that's joins or multiple round-trips. In DynamoDB with a single-table design, it's one &lt;code&gt;Query&lt;/code&gt; call per page — no joins, no N+1 problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Serverless + connection pools don't mix well.&lt;/strong&gt; Vercel functions are stateless and short-lived. Every cold start to a Postgres database burns 30–80ms negotiating a connection before the first query runs. DynamoDB's HTTP API is connectionless by design — it's a natural fit for serverless compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. On-demand capacity.&lt;/strong&gt; At launch, I have no idea how many creators will sign up. DynamoDB on-demand scales to zero (no idle cost) and scales up with zero capacity planning. For a hackathon-born product, that's the right default.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Single-Table Schema
&lt;/h2&gt;

&lt;p&gt;Everything lives in one DynamoDB table. Partition key: &lt;code&gt;PK&lt;/code&gt; (String). Sort key: &lt;code&gt;SK&lt;/code&gt; (String).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK                        SK                              Entity
─────────────────────     ──────────────────────────      ──────────────────
CREATOR#&amp;lt;userId&amp;gt;          METADATA                        Creator profile
CREATOR#&amp;lt;userId&amp;gt;          ASSET#&amp;lt;videoId&amp;gt;                 Uploaded video
CREATOR#&amp;lt;userId&amp;gt;          ASSET#&amp;lt;videoId&amp;gt;#CHAPTERS        AI-extracted chapters
CREATOR#&amp;lt;userId&amp;gt;          CLIP#&amp;lt;clipId&amp;gt;                   Extracted clip
CREATOR#&amp;lt;userId&amp;gt;          STREAM#&amp;lt;streamId&amp;gt;               Live stream record
CREATOR#&amp;lt;userId&amp;gt;          LIVE_CHAT_SPIKE#&amp;lt;timestamp&amp;gt;     Chat engagement spike
CREATOR#&amp;lt;userId&amp;gt;          ANALYTICS#DAILY#&amp;lt;date&amp;gt;          Daily metrics
CREATOR#&amp;lt;userId&amp;gt;          PLATFORM_TOKEN#&amp;lt;platform&amp;gt;       OAuth token
MAGIC#&amp;lt;token&amp;gt;             VERIFY                          Magic link token (TTL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every entity for a given user shares the same partition key. Queries use &lt;code&gt;begins_with(SK, prefix)&lt;/code&gt; to retrieve a specific entity type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// All clips for a user&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;docClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;KeyConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PK = :pk AND begins_with(SK, :sk)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:pk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`CREATOR#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:sk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CLIP#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No GSIs. No secondary indexes. All access patterns are served by this one schema.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Single-table design has costs worth naming honestly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hot partitions are a real risk at scale.&lt;/strong&gt; If one creator has 50,000 clips, their partition takes all the heat. DynamoDB on-demand handles burst well, but you can't fully escape this. For V2, I'd add a shard suffix to &lt;code&gt;PK&lt;/code&gt; for high-volume entities — &lt;code&gt;CREATOR#&amp;lt;userId&amp;gt;#CLIPS#&amp;lt;shard&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose ad-hoc querying.&lt;/strong&gt; There's no &lt;code&gt;SELECT * FROM clips WHERE viralityScore &amp;gt; 80&lt;/code&gt;. Any access pattern you didn't model upfront requires a scan or a GSI. I've been careful to design the app's UI around the access patterns I built, not the other way around. That discipline is uncomfortable at first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magic link tokens use a different partition scheme&lt;/strong&gt; (&lt;code&gt;MAGIC#&amp;lt;token&amp;gt;&lt;/code&gt;) because they need to be looked up by token, not by user. Mixing these in the same table is fine — it's by design in single-table modeling — but it requires deliberate thought about what "partition" means for each entity type.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Vercel Side
&lt;/h2&gt;

&lt;p&gt;The frontend is Next.js App Router on Vercel. A few things worth noting for other developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route protection runs at the edge.&lt;/strong&gt; A &lt;code&gt;proxy.ts&lt;/code&gt; middleware checks the NextAuth session cookie (&lt;code&gt;authjs.session-token&lt;/code&gt;) before any server component renders. Unauthenticated users get redirected to &lt;code&gt;/login&lt;/code&gt; at the CDN layer — no compute wasted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video uploads bypass Vercel entirely.&lt;/strong&gt; The browser requests a presigned S3 PUT URL from an API route, then uploads directly to S3. Vercel never sees a raw video byte. This avoids Vercel's response size limits and keeps egress costs at zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential federation, not static keys.&lt;/strong&gt; In production, there are no &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; environment variables. Vercel injects an OIDC token; the app calls &lt;code&gt;sts:AssumeRoleWithWebIdentity&lt;/code&gt; to get short-lived credentials, cached in-process with a 60-second expiry buffer. This took an afternoon to set up and eliminated an entire category of credential-leak risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;The analysis pipeline is currently synchronous — it blocks the HTTP request while Gemini processes the video. For anything longer than a short clip, that's a race against Vercel's function timeout. The next version will push analysis to a background queue (Vercel Queues) with status polling.&lt;/p&gt;

&lt;p&gt;DynamoDB queries also have no pagination yet. For a creator with thousands of clips, every page load fetches the full partition. Adding &lt;code&gt;Limit&lt;/code&gt; + &lt;code&gt;ExclusiveStartKey&lt;/code&gt; is straightforward — it just wasn't the bottleneck at hackathon scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Truss is live at &lt;a href="https://the-truss-app.vercel.app" rel="noopener noreferrer"&gt;the-truss-app.vercel.app&lt;/a&gt;. The stack — Next.js on Vercel, single-table DynamoDB, S3 for object storage, Vercel AI SDK for Gemini — held up well under the build pressure of a hackathon.&lt;/p&gt;

&lt;p&gt;If you're a developer considering this stack for a B2C SaaS: the single-table DynamoDB pattern has a real learning curve, but once your access patterns click into the schema, the operational simplicity pays for the upfront modeling cost many times over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#H0Hackathon&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>database</category>
      <category>saas</category>
    </item>
    <item>
      <title>Machine Learning on AWS: A Brief Introduction</title>
      <dc:creator>Joyce</dc:creator>
      <pubDate>Fri, 26 Jan 2024 21:53:08 +0000</pubDate>
      <link>https://dev.to/jwambui/machine-learning-on-aws-a-brief-introduction-a0h</link>
      <guid>https://dev.to/jwambui/machine-learning-on-aws-a-brief-introduction-a0h</guid>
      <description>&lt;p&gt;In this fast-paced world of technology, businesses are constantly seeking innovative solutions to stay ahead of the curve. One such revolutionary technology that has gained immense popularity in recent years is machine learning. I have been working on Machine Learning projects on AWS and recently got my &lt;a href="https://aws.amazon.com/certification/certified-machine-learning-specialty/" rel="noopener noreferrer"&gt;AWS Machine Learning - Specialty Certification&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this short blog post, I will introduce the basic ecosystem of Machine Learning on AWS. I plan to delve deeper in future posts, including an end to end project, so stay tuned!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data: S3, Athena, AWS Glue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Data is a very important aspect in a machine learning project. &lt;br&gt;
AWS provides tools such as Amazon S3 for scalable storage, AWS Glue for data preparation, and Amazon Athena for querying data directly in S3. These services create a robust foundation for data-driven decision-making and model training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foundations : Amazon Sagemaker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the heart of machine learning on AWS. Amazon Sagemaker is a fully managed service that allows developers and data scientists to build, train and deploy machine learning models at scale. With support for machine learning frameworks like Tensorflow and pyTorch, Sagemaker simplifies the entire machine learning workflow. It also supports governance requirements with simplified access control and transparency over your ML projects.&lt;/p&gt;

&lt;p&gt;Data scientists can leverage SageMaker's easy-to-use interface to experiment with different algorithms, hyperparameters, and data sets. The service also supports distributed training, enabling faster model convergence.&lt;/p&gt;

&lt;p&gt;Once a model is trained, deploying it for real-world use is seamless with SageMaker Hosting. Whether you want to deploy models for batch processing or real-time predictions, AWS provides scalable and cost-effective solutions that automatically handle the underlying infrastructure, allowing developers to focus on their applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security and Compliance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With features like VPCs (Virtual Private Cloud) and encryption at rest and in transit, businesses can ensure the confidentiality and integrity of their machine learning workloads. &lt;/p&gt;

&lt;p&gt;I hope this short overview piqued your interest in the wonderful world of machine learning on AWS. I am truly excited about the next posts as we shall venture deeper into these technologies and build something amazing!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
