Blueprints/Build a feature voting board

Build a feature voting board

Users vote. A bar chart shows what is winning. Two API calls to wire it up.

Updated March 2026

You want to know what to build next. So you let users vote. Clerk handles login, Cloudflare Pages hosts the site for free, and Sutrena stores the votes and shows a bar chart of what is ahead. Each user gets one vote per feature. Total cost: $9/month. An agent can set this up end-to-end with a sequence of API calls: create the form with dedup, build the dashboard with a bar chart, deploy the voting page.

Architecture

ToolRoleCost
ClerkAuthentication (user identity)Free (50K MAU)
Cloudflare PagesStatic site hostingFree
SutrenaVote storage, dedup, dashboard$9/mo (Builder)

Total cost: $9/mo

Builder starts at $9/month for 50 projects. Pro at $29/month covers 200 projects and all your other work too. Build five blueprints on Pro and it is still $29/month. That is where the value is — at scale.

Clerk gives each user a stable user_id. Your React app sends votes to Sutrena with user_id and feature_id as hidden fields. Sutrena deduplicates on the combination, so one vote per user per feature. The dashboard groups by feature_id to make a leaderboard.

The flow: user clicks upvote, frontend POSTs to the Sutrena submit endpoint with the Clerk user_id, dashboard updates within 30 seconds. No server-side code needed.

Form Definition

Two hidden fields: user_id from Clerk, feature_id for the feature being voted on. uniqueBy makes sure one vote per user per feature. publicResults lets the frontend fetch counts without auth.

{
  "name": "Feature Votes",
  "fields": [
    {
      "name": "user_id",
      "type": "hidden"
    },
    {
      "name": "feature_id",
      "type": "hidden"
    },
    {
      "name": "vote",
      "label": "Vote",
      "type": "select",
      "options": [
        "upvote"
      ],
      "required": true
    }
  ],
  "uniqueBy": [
    "user_id",
    "feature_id"
  ],
  "publicResults": true
}

Dashboard Definition

Total votes, a bar chart ranking features by count, and a table of recent votes. The bar chart is the one you care about -- it shows what is winning at a glance.

{
  "version": 1,
  "widgets": [
    {
      "type": "metric_card",
      "title": "Total Votes",
      "value": "count(*)"
    },
    {
      "type": "bar_chart",
      "title": "Votes by Feature",
      "groupBy": "feature_id"
    },
    {
      "type": "data_table",
      "title": "Recent Votes",
      "columns": [
        "user_id",
        "feature_id",
        "vote"
      ],
      "limit": 50,
      "sortBy": "$submitted_at",
      "sortOrder": "desc"
    }
  ]
}

Frontend Integration

Gets the user_id from Clerk's useUser hook, POSTs it to Sutrena with the feature_id. Already voted? Sutrena returns 409, the component shows a message. Replace frm_YOUR_FORM_ID with your actual form ID.

"use client";
import { useUser } from "@clerk/nextjs";
import { useState } from "react";

const FORM_ID = "frm_YOUR_FORM_ID";

export function VoteButton({ featureId }: { featureId: string }) {
  const { user } = useUser();
  const [voted, setVoted] = useState(false);
  const [error, setError] = useState("");

  async function handleVote() {
    if (!user) return;
    setError("");

    const res = await fetch(
      `https://sutrena.com/api/forms/${FORM_ID}/submit`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          user_id: user.id,
          feature_id: featureId,
          vote: "upvote",
        }),
      }
    );

    if (res.ok) {
      setVoted(true);
    } else if (res.status === 409) {
      setError("You already voted for this feature.");
    }
  }

  if (voted) return <span>Voted!</span>;

  return (
    <div>
      <button onClick={handleVote} disabled={!user}>
        Upvote
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

FAQ

Can users change their vote?

Not directly. uniqueBy rejects duplicates. To allow changes, delete the old submission via the API first, then submit the new one. For a simple upvote board, one vote per user per feature is usually enough.

How do I display vote counts on the frontend?

publicResults is true, so you can fetch the submissions API or dashboard data to count votes per feature_id. Or just link to the public dashboard URL -- it has a live bar chart already.

Can I use this without Clerk?

Yes. Any auth provider that gives you a stable user ID works. You could also use email as the user_id and deduplicate on email + feature_id. Depends on what you have.

What happens if someone tries to vote without signing in?

The button is disabled when there is no user. You can also redirect to Clerk's sign-in page if you prefer that flow.

What is Sutrena?

Sutrena is the web runtime for AI agents. Three primitives — pages, forms, and dashboards — accessible through one API. Your agent creates web artifacts, humans interact with them, and your agent gets the data back. Framework-agnostic. Works from any MCP client or HTTP client.

Get started in two API calls

1. Get a trial key (no auth, no signup)

curl -X POST https://sutrena.com/api/trial

2. Create a form + dashboard from a template

curl -X POST https://sutrena.com/api/forms \
  -H "Authorization: Bearer st_trial_xxx" \
  -H "Content-Type: application/json" \
  -d '{"templateId": "waitlist", "createDashboard": true}'

Ready to build?

Get a trial API key instantly with no signup, or create an account for the full experience.

Build a feature voting board — Sutrena | Sutrena