I Built a Global Opinion Platform in 72 Hours — Here Is What Actually Went Wrong

I Built a Global Opinion Platform in 72 Hours — Here Is What Actually Went Wrong

我在 72 小时内构建了一个全球民意平台——以下是实际遇到的坑

I finished the Groundswell hackathon build with a working product, a deployed URL, and a list of things I wish I had known before I started. This is that list. Not the polished version where everything went according to plan — the actual version, including the three hours I spent debugging a database connection that was failing for a reason I had completely wrong. 我完成了 Groundswell 黑客松的开发,交付了一个可用的产品、一个已部署的 URL,以及一份我希望在开始前就知道的清单。这就是那份清单。它不是那种一切按计划进行的“美化版”,而是真实版本,包括我花了三个小时调试数据库连接,却因为完全搞错了原因而失败的经历。

Groundswell is a global daily opinion platform where players vote on a question and then predict how different countries and age groups voted. Your score comes from prediction accuracy, not opinion alignment. Building it in 72 hours using Next.js, Aurora DSQL, DynamoDB, and Amazon Bedrock taught me more about AWS database architecture than I expected. Most of what I learned came from mistakes. Groundswell 是一个全球每日民意平台,玩家可以对问题进行投票,然后预测不同国家和年龄段的人群是如何投票的。你的得分取决于预测的准确性,而不是观点的趋同性。在 72 小时内使用 Next.js、Aurora DSQL、DynamoDB 和 Amazon Bedrock 构建这个平台,让我对 AWS 数据库架构的了解超出了预期。我学到的大部分知识都源于错误。

Mistake 1 — Assuming Aurora DSQL Works Like Regular PostgreSQL

错误 1 — 假设 Aurora DSQL 的工作方式与普通 PostgreSQL 相同

My mental model going in was that Aurora DSQL was PostgreSQL with a fancier name and better global distribution. It is not. It has a PostgreSQL-compatible connection interface, which means your queries look the same, but several things work differently and some features that standard PostgreSQL has are missing entirely. 我最初的心理模型是,Aurora DSQL 就是一个名字更高级、全球分布更好的 PostgreSQL。事实并非如此。它拥有兼容 PostgreSQL 的连接接口,这意味着你的查询语句看起来一样,但很多底层逻辑不同,且标准 PostgreSQL 的某些功能完全缺失。

The first problem was authentication. Aurora DSQL does not accept a static database password. Connection requires a short-lived token generated from your AWS credentials each time you connect. When running locally, this token comes from the Vercel CLI when you run the app with vercel dev. If you run the standard npm run dev command instead, the token is never injected, and every database connection attempt returns an access denied error. I spent several hours convinced my environment variables were wrong before realizing the issue was which command I used to start the server. 第一个问题是身份验证。Aurora DSQL 不接受静态数据库密码。连接时需要每次从你的 AWS 凭证生成一个短期令牌。在本地运行时,当你使用 vercel dev 运行应用时,该令牌由 Vercel CLI 提供。如果你改用标准的 npm run dev 命令,令牌就不会被注入,每次数据库连接尝试都会返回“拒绝访问”错误。我花了几个小时确信是环境变量配置错了,最后才意识到问题出在我启动服务器所用的命令上。

The second problem was schema creation. Foreign key constraints are not supported in Aurora DSQL. I had to remove every REFERENCES constraint from my table definitions and handle referential integrity in the application layer instead. Index creation requires an ASYNC keyword that standard PostgreSQL does not use. Descending sort orders on indexes are not supported. Each of these discoveries happened at the moment of running the migration, not before. The fixes were all straightforward once I understood what the actual limitations were. But discovering them during a time-constrained build was expensive. If you are building with Aurora DSQL, read the limitations section of the documentation before you write your schema. 第二个问题是模式(Schema)创建。Aurora DSQL 不支持外键约束。我不得不从表定义中删除了所有的 REFERENCES 约束,转而在应用层处理引用完整性。索引创建需要一个标准 PostgreSQL 不使用的 ASYNC 关键字。索引不支持降序排序。这些发现都是在运行迁移时才出现的,而不是之前。一旦我理解了实际的限制,修复方法就很简单。但在时间紧迫的开发过程中发现这些问题代价高昂。如果你正在使用 Aurora DSQL 进行开发,请在编写模式之前阅读文档中的限制部分。

Mistake 2 — Planning for Tables That Did Not Exist Yet

错误 2 — 为尚不存在的表进行规划

The original DynamoDB design for Groundswell had two separate tables. One for individual vote records to prevent duplicate submissions. One for demographic segment counters to track yes and no counts by country and age group. The separation felt clean on paper. Groundswell 最初的 DynamoDB 设计包含两个独立的表。一个用于存储个人投票记录以防止重复提交;另一个用于存储人口统计细分计数器,以跟踪不同国家和年龄组的“赞成”和“反对”票数。这种分离在纸面上看起来很清晰。

What I did not know until I tried to create those tables programmatically is that the IAM role provisioned by the Vercel DynamoDB Marketplace integration is scoped specifically to the single table that Vercel creates automatically. Attempts to create additional tables returned access denied errors. The permissions simply did not extend beyond the auto-provisioned table. 直到我尝试以编程方式创建这些表时,我才知道 Vercel DynamoDB Marketplace 集成所配置的 IAM 角色,其权限范围仅限于 Vercel 自动创建的那一个表。尝试创建额外的表会返回“拒绝访问”错误。权限根本无法扩展到自动配置的表之外。

The solution was to redesign the schema as a single-table design on the Vercel-managed table, using partition key prefixes to distinguish between item types. Vote records use a VOTE prefix. Segment counters use a COUNTER prefix. Both live in the same table and are accessed through different query patterns that never conflict. What felt like a constraint turned into the correct architectural decision. Single-table design is the pattern AWS recommends for DynamoDB when access patterns are well-understood. The constraint removed an option I should not have been using anyway and pushed me toward the better solution. 解决方案是将模式重新设计为 Vercel 管理表上的单表设计,使用分区键前缀来区分项目类型。投票记录使用 VOTE 前缀,细分计数器使用 COUNTER 前缀。两者都存在于同一个表中,并通过从不冲突的不同查询模式进行访问。原本感觉像是一种限制,结果却成了正确的架构决策。当访问模式明确时,单表设计是 AWS 为 DynamoDB 推荐的模式。这种限制排除了我本不该使用的选项,并推动我走向了更好的解决方案。

Mistake 3 — Underestimating How Different Concurrent Writes and Relational Reads Actually Are

错误 3 — 低估了并发写入与关系型读取之间的巨大差异

Going into the build I knew I needed a database for votes and a database for user profiles and leaderboards. What I underestimated was how incompatible those two requirements actually are at the level of database internals. 在开始构建时,我知道我需要一个数据库来处理投票,另一个数据库来处理用户资料和排行榜。但我低估了这两个需求在数据库内部层面上的不兼容性。

The vote ingestion pattern is extremely write-heavy with high concurrency. Thousands of players potentially updating the same demographic counter rows at the same time. DynamoDB’s atomic ADD operation handles this natively — every increment happens correctly without any locking or transaction coordination. The same operation on a relational database under the same load would produce lock contention that grows with concurrency. 投票摄取模式是极高并发的写入密集型。成千上万的玩家可能同时更新同一个人口统计计数器行。DynamoDB 的原子 ADD 操作可以原生处理这种情况——每次增量都能正确执行,无需任何锁定或事务协调。在同样的负载下,关系型数据库上的相同操作会产生随并发性增加而加剧的锁竞争。

The leaderboard and scoring patterns require SQL. The global ranking uses a window function that computes each player’s position relative to all other players in a single query. The prediction scoring joins two tables to compute error points for every player in one operation. These are not operations you can replicate in DynamoDB without significant application-layer workarounds that become slower as the data grows. 排行榜和评分模式需要 SQL。全局排名使用窗口函数,在单个查询中计算每个玩家相对于所有其他玩家的位置。预测评分通过连接两个表,在一次操作中计算每个玩家的误差分数。这些操作如果不进行大量的应用层变通,是无法在 DynamoDB 中复制的,而且随着数据量的增长,这些变通方法会变得越来越慢。

Once I understood those requirements clearly, the database separation became obvious rather than optional. DynamoDB for the writes. Aurora DSQL for the reads. A nightly pipeline that moves data from one to the other. The architecture that emerged from following the access patterns strictly is the one I would design the same way from the start if I were doing it again. 一旦我清楚地理解了这些需求,数据库分离就变得显而易见,而非可有可无。DynamoDB 用于写入,Aurora DSQL 用于读取。通过一个每晚运行的管道将数据从前者迁移到后者。这种严格遵循访问模式而形成的架构,如果让我重来一次,我依然会从一开始就采用这种设计。

What Actually Surprised Me in a Good Way

真正让我感到惊喜的是

The Bedrock integration for question generation was simpler than I expected. A prompt describing the type of question I needed, a structured JSON format for the response, and some string parsing to remove the markdown wrappers the model adds around its output. The model returns five candidate questions with predicted divergence scores. An admin reviews and approves one before it goes live. The whole thing works reliably. 用于问题生成的 Bedrock 集成比我预期的要简单。只需一个描述我所需问题类型的提示词、一个结构化的 JSON 响应格式,以及一些用于去除模型在输出周围添加的 Markdown 包装的字符串解析。模型会返回五个候选问题及其预测的分歧得分。管理员在发布前审核并批准其中一个。整个流程运行非常可靠。

The Vercel Marketplace provisioning for both databases genuinely took minutes. Environment variables were injected automatically. The infrastructure was ready before I finished reading the documentation. Vercel Marketplace 为两个数据库提供的配置确实只用了几分钟。环境变量是自动注入的。在我读完文档之前,基础设施就已经准备好了。