How to Build a Short-Lived Survey App Without Keeping a Backend Running

Some surveys do not need a database, an admin panel that lives forever, and a backend you have to remember to shut down later.

Sometimes you need a survey for a workshop, a live event, a support session, or a one-off round of feedback. In that case, the survey needs to start quickly, give you a shareable link, collect responses for a while, and disappear when the session is over.

This demo is built around that idea. The survey is created in the outer app, written into a BrowserPod runtime, and served from there. Responses live in memory. Close the tab and the survey is gone.

Build the survey in the outer app first

The outer app collects the survey title, description, and question list before BrowserPod even boots.

When the form is submitted, the demo turns those fields into one object with a survey ID and an admin token:

const surveyData = {
	title,
	description,
	questions,
	surveyId: `survey_${Date.now()}`,
	adminToken: Math.random().toString(36).substring(2, 15),
};

The browser UI handles survey creation. The pod only needs to serve the survey that already exists.

Boot the pod and write the survey into /project

Once the survey data is ready, the demo follows the normal BrowserPod startup order:

pod = await BrowserPod.boot({ apiKey: import.meta.env.VITE_BP_APIKEY });
await new Promise((resolve) => setTimeout(resolve, 500));

const terminal = await pod.createDefaultTerminal(
	document.querySelector("#console")
);
await pod.createDirectory("/project");
await pod.createDirectory("/project/public");

Then it copies the survey runtime into the pod:

await copyFile(pod, "project/main.js");
await copyFile(pod, "project/package.json");
await copyFile(pod, "project/public/app.html");
await copyFile(pod, "project/public/survey.html");
await copyFile(pod, "project/public/admin.html");
await copyFile(pod, "project/public/results.html");

After that, it writes the live survey config into a file the runtime can import:

const surveyDataJson = JSON.stringify(surveyData);
const surveyDataContent = `module.exports = ${surveyDataJson};`;
const surveyDataFile = await pod.createFile(
	"/project/survey-data.js",
	"binary"
);
const encoder = new TextEncoder();
const encodedData = encoder.encode(surveyDataContent);
const buffer = encodedData.buffer.slice(
	encodedData.byteOffset,
	encodedData.byteOffset + encodedData.byteLength
);
await surveyDataFile.write(buffer);
await surveyDataFile.close();

survey-data.js is the handoff between the outer app and the runtime. The outer app defines the survey. The runtime starts with that survey already active.

Install dependencies, start the server, and wait for the portal URL

Once the files are in place, the demo installs dependencies and starts the Express server inside /project:

await pod.run("npm", ["install"], {
	echo: false,
	terminal: terminal,
	cwd: "/project",
});
pod.run("node", ["main.js"], {
	echo: false,
	terminal: terminal,
	cwd: "/project",
});

Then it waits for BrowserPod to expose the server:

pod.onPortal(({ url, port }) => {
	const adminUrl = `${url}/admin/${surveyData.surveyId}?token=${surveyData.adminToken}`;

	document.getElementById("app-container").style.display = "none";
	document.getElementById("portal-container").style.display = "block";
	document.getElementById("portal").src = adminUrl;

	showSurveySuccess({
		surveyId: surveyData.surveyId,
		clientUrl: `${url}/survey/${surveyData.surveyId}`,
		adminUrl: adminUrl,
		portalUrl: url,
	});
});

From that one URL, the demo creates:

  • the client survey link
  • the admin dashboard link
  • the iframe view for the survey owner

Keep the survey state in memory on purpose

The Express runtime inside public/project/main.js keeps survey data in a plain in-memory store:

const store = {
	surveys: {},
	nextId: 1,
};

It initializes that store from survey-data.js at startup:

try {
	const surveyDataModule = require("./survey-data.js");
	if (surveyDataModule && surveyDataModule.surveyId) {
		initializeSurvey(surveyDataModule);
	}
} catch (e) {
	console.log("Survey data file not available yet or error:", e.message);
}

The survey is temporary by design. Responses survive only as long as that BrowserPod session is alive.

The runtime exposes separate client, admin, and results views

Once the survey is loaded, the runtime serves a few different pages and APIs around it.

The client survey is available at /survey/:surveyId. The admin dashboard is available at /admin/:surveyId?token=.... The results view is available at /results/:surveyId.

Responses are submitted through the API:

app.post("/api/surveys/:surveyId/responses", (req, res) => {
	const survey = store.surveys[req.params.surveyId];
	const errors = validateResponse(survey, req.body);
	if (errors.length > 0) {
		return res.status(400).json({ errors });
	}

	const response = {
		id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
		data: req.body,
		submittedAt: new Date().toISOString(),
	};

	survey.responses.push(response);
	res.json({ success: true, responseId: response.id });
});

Results are aggregated on the server so the admin view can render counts, free-text answers, and scale averages:

app.get("/api/surveys/:surveyId/results", (req, res) => {
	const survey = store.surveys[req.params.surveyId];

	const results = {
		surveyId: survey.id,
		title: survey.title,
		totalResponses: survey.responses.length,
		questions: survey.questions.map((question) => {
			// aggregate by question type
		}),
	};

	res.json(results);
});

The runtime also supports CSV export and QR-code generation, which fits an event-style survey where the organizer needs a share link immediately and wants the results back out quickly.

This demo does not guess the final survey URL before the server exists. It waits for the portal URL and derives the participant link from that:

showSurveySuccess({
	surveyId: surveyData.surveyId,
	clientUrl: `${url}/survey/${surveyData.surveyId}`,
	adminUrl: adminUrl,
	portalUrl: url,
});

The link in the success modal, the QR code, and the admin iframe all come from the live server URL, not from a local placeholder.

Where this approach fits

Use this when the survey is tied to a session, not to a permanent service.

Examples:

  • collecting workshop feedback during a live event
  • opening a short-lived survey during a support call
  • gathering responses during a product demo
  • spinning up a temporary admin view for a one-off round of feedback

If you need permanent storage, recurring campaigns, or analytics that survive restarts, use a different system. If you need a survey that starts now, gives you a share link, and disappears with the session, this setup fits.

Ready to get started?

Spin up your first sandbox in minutes. No credit card required.