◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-04-02Beginner

Building a Survey App with Rork — A Beginner's Guide to Forms, Data Collection, and Charts

Learn how to build a survey collection app from scratch using Rork. This beginner-friendly guide covers form design, multiple answer types, Supabase data storage, and aggregation charts — all in a single day.

surveyformRork585Supabase37React Native239beginner21data visualization

Survey apps are incredibly practical — whether you're collecting customer feedback, running internal polls, or gathering event responses. Building a dedicated mobile app instead of relying on spreadsheets makes the experience far more pleasant for respondents, and gives you full control over the data.

Here you build a fully functional survey app with Rork — multiple question types (single choice, multiple choice, and free text), responses saved to Supabase, and aggregated results drawn as a bar chart. It is written for beginners: you don't need deep React Native knowledge, just a basic familiarity with Rork's interface.

What You'll Build

By the end of this tutorial, your app will include:

  • A survey form supporting single-choice, multiple-choice, and free-text questions
  • Response storage in Supabase via its REST API
  • An aggregation dashboard showing response counts and percentages as bar charts
  • Basic duplicate-response prevention using a device identifier

Prerequisites and Setup

Before diving in, make sure you have the following ready:

  • A Rork account (the free plan is sufficient for this project)
  • A Supabase account (free tier works fine — you'll need the project URL and anon key)
  • A rough idea of your survey questions, options, and expected answer types

If you haven't created a Supabase project yet, go ahead and do that first. The setup only takes a couple of minutes.

App Architecture Overview

Screen Structure

The app consists of three screens:

  • Home screen — Displays the survey title, description, and a "Start Survey" button
  • Form screen — Shows questions one at a time in a step-by-step flow
  • Dashboard screen — Visualizes aggregated responses with bar charts

Database Schema

You'll need two tables in Supabase:

surveys
  id          uuid (primary key)
  title       text
  description text
  created_at  timestamp

responses
  id            uuid (primary key)
  survey_id     uuid (foreign key → surveys)
  question_id   text
  answer        text
  respondent_id text
  created_at    timestamp

The answer field stores a plain string for single-choice and free text, and a comma-separated string for multiple-choice responses. This keeps the schema simple while covering all common cases.

Step-by-Step Implementation

Step 1: Set Up Supabase Tables

Open the SQL editor in your Supabase dashboard and run the following:

-- Create the surveys table
CREATE TABLE surveys (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  title text NOT NULL,
  description text,
  created_at timestamp DEFAULT now()
);
 
-- Create the responses table
CREATE TABLE responses (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  survey_id uuid REFERENCES surveys(id) ON DELETE CASCADE,
  question_id text NOT NULL,
  answer text NOT NULL,
  respondent_id text,
  created_at timestamp DEFAULT now()
);
 
-- Enable Row-Level Security and allow public access for this demo
ALTER TABLE surveys ENABLE ROW LEVEL SECURITY;
ALTER TABLE responses ENABLE ROW LEVEL SECURITY;
 
CREATE POLICY "Public surveys are viewable" ON surveys FOR SELECT USING (true);
CREATE POLICY "Anyone can submit responses" ON responses FOR INSERT WITH CHECK (true);
CREATE POLICY "Responses are viewable" ON responses FOR SELECT USING (true);

Once the tables are created, copy your project URL and anon key from Settings → API.

Step 2: Create the Project in Rork

Open Rork, create a new project, and use this prompt to get started:

Build a survey collection app with the following features:

1. A home screen showing the survey title, description, and a "Start Survey" button
2. A step-by-step form screen that shows one question at a time
3. Three question types: single choice (radio buttons), multiple choice (checkboxes),
   and free text (text input)
4. A "Submit" button at the end that saves responses to Supabase
5. A dashboard screen showing response counts and percentage bars per option

Use a clean, blue-themed color scheme.

Rork will generate the screen structure automatically. Review the UI and refine as needed before wiring up the data layer.

Step 3: Define Your Survey Questions

Add a questions.ts file to store your question data:

// questions.ts — Survey question definitions
export const SURVEY_QUESTIONS = [
  {
    id: "q1",
    type: "single", // single-choice
    text: "How would you describe your app development experience?",
    options: ["No experience", "Self-teaching", "Published personal apps", "Professional developer"],
  },
  {
    id: "q2",
    type: "multiple", // multiple-choice
    text: "Which no-code/AI app builders have you tried? (Select all that apply)",
    options: ["Rork", "FlutterFlow", "Adalo", "Bubble", "Bolt", "None"],
  },
  {
    id: "q3",
    type: "text", // free-text
    text: "What's your biggest challenge when building apps?",
  },
];

Step 4: Save Responses to Supabase

Set up the Supabase client and a submit function:

// supabase.ts — Supabase client and response submission
import { createClient } from "@supabase/supabase-js";
 
const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!
);
 
export async function submitSurveyResponses(
  surveyId: string,
  answers: Record<string, string | string[]>,
  respondentId: string
) {
  const rows = Object.entries(answers).map(([questionId, answer]) => ({
    survey_id: surveyId,
    question_id: questionId,
    // Join arrays (multiple choice) into a comma-separated string
    answer: Array.isArray(answer) ? answer.join(",") : answer,
    respondent_id: respondentId,
  }));
 
  const { error } = await supabase.from("responses").insert(rows);
 
  if (error) {
    console.error("Failed to save responses:", error.message);
    throw error;
  }
 
  return { success: true };
}

Use a device-generated UUID stored in AsyncStorage as the respondentId to prevent duplicate submissions from the same device.

Ask Rork to wire up the submit button with this prompt:

When the user taps Submit, call submitSurveyResponses() with the collected answers
and respondentId. Show a loading spinner during submission. On success, navigate to
a "Thank You" screen. On error, show an alert with the error message.

Step 5: Build the Results Dashboard

Fetch and aggregate response data with a custom hook:

// useResponseStats.ts
import { useEffect, useState } from "react";
import { supabase } from "./supabase";
 
interface AnswerStat {
  option: string;
  count: number;
  percentage: number;
}
 
export function useResponseStats(surveyId: string, questionId: string) {
  const [stats, setStats] = useState<AnswerStat[]>([]);
  const [total, setTotal] = useState(0);
 
  useEffect(() => {
    async function fetchStats() {
      const { data } = await supabase
        .from("responses")
        .select("answer")
        .eq("survey_id", surveyId)
        .eq("question_id", questionId);
 
      if (!data) return;
 
      const countMap: Record<string, number> = {};
      data.forEach(({ answer }) => {
        answer.split(",").forEach((opt: string) => {
          const key = opt.trim();
          countMap[key] = (countMap[key] || 0) + 1;
        });
      });
 
      const totalCount = data.length;
      setTotal(totalCount);
      setStats(
        Object.entries(countMap).map(([option, count]) => ({
          option,
          count,
          percentage: Math.round((count / totalCount) * 100),
        }))
      );
    }
    fetchStats();
  }, [surveyId, questionId]);
 
  return { stats, total };
}

For the charting layer, Building Interactive Charts in Your Rork App — Victory Native Guide walks through exactly how to render bar and pie charts from this kind of aggregated data.

Common Errors and Fixes

"Row-level security policy violation" — This means the RLS policies weren't applied correctly. Re-run the CREATE POLICY SQL statements from Step 1 and confirm they're listed under Authentication → Policies in the Supabase dashboard.

"Cannot read property of undefined" — This usually means the component rendered before data was ready. Make sure your hooks provide sensible default values ([] for arrays, 0 for counts) and show a loading indicator while data is being fetched.

Duplicate responses being saved — Double-check that your respondentId is being generated once and persisted in AsyncStorage. If it's re-generated on every app launch, the same user will appear as a new respondent each time.

Ideas for Extending the App

Once the core app is working, here are some directions worth exploring:

  • Dynamic question management: Move question data to a Supabase table so you can edit surveys without redeploying the app
  • Submission deadline: Add an expires_at field to limit when responses can be submitted
  • CSV export: Let admins download response data as a spreadsheet — this is highly valued in business settings
  • SaaS expansion: Turn it into a multi-survey platform with user accounts, and charge a monthly subscription fee. For building real-time dashboards on top of Supabase, Building a Real-Time Analytics Dashboard with Rork Max and Supabase Realtime is a great next read.

Wrapping Up

Building a survey app with Rork is a great way to get hands-on experience with forms, data persistence, and data visualization all in one project. Here's a quick recap of what we covered:

  • Setting up Supabase tables with RLS policies for public surveys
  • Generating the form UI in Rork and connecting it to your question definitions
  • Saving responses to Supabase and preventing duplicate submissions
  • Fetching aggregated data and rendering it as a dashboard

The survey app you built today is a solid foundation — take it further by adding dynamic question management, deadlines, and eventually a full SaaS model with authentication and billing.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.