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.
In this guide, you'll build a fully functional survey app using Rork. We'll cover multiple question types (single choice, multiple choice, and free text), saving responses to Supabase, and displaying aggregated results as a bar chart. This 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_atfield 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.