Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Survey Platform — Senior Fullstack Take-Home

Target time: 2-4 hours. AI assistance is allowed and encouraged. Knowing when to stop is part of the evaluation.


The story

Our platform helps field researchers in dozens of countries collect survey data — health assessments, water-quality checks, school enrollment counts, that kind of thing. Central staff at HQ publish the surveys; researchers in the field fill them out.

You're building the part that tracks who's working on what. When a team member starts a survey, the system records their name and a due date. While that draft is open:

  • The same team member can't start a second draft on the same survey.
  • A different team member can still start their own — two researchers can work on the same survey in parallel.

When they finish, they finalize the draft. They can start a fresh one later if needed.

Every start is recorded in an audit log. If writing that log ever fails, the whole submission should be undone — we can't have submissions that aren't audited.


What this is

A small survey-tracking API + thin web UI. The Survey entity is fully working as a reference. Your task is to add a Submission feature that mirrors Survey's layering pattern, with two specific mechanics layered on top:

  1. Clean repo / service / resolver layering — repositories return UnsecuredDto<T>; services orchestrate; resolvers are thin.
  2. Transactional hooks — when a submission is started, a hook fires inside the same DB transaction as the mutation, and a handler writes an audit row. If anything in the handler throws, the submission insert rolls back.

These two mechanics mirror patterns we use in our production codebase. The take-home strips away the heavier patterns (RBAC, secured-field wrapping) to keep scope realistic.


Stack

  • API: NestJS 11 (Fastify) · TypeScript · GraphQL Yoga (code-first via @nestjs/graphql) · Drizzle ORM · PostgreSQL 16
  • Web: Next.js 15 (App Router) · Apollo Client · Tailwind
  • Container: Docker Compose (Postgres only — the apps run on your host)

Setup

Prereqs: Node 20+, Docker, npm.

docker compose up -d                      # postgres on :5433

cd api
cp .env.example .env
npm install
npm run db:migrate                        # creates surveys + audit_events
npm run db:seed                           # 10 sample surveys
npm run dev                               # http://localhost:4000/graphql

# in another shell
cd web
cp .env.example .env.local
npm install
npm run dev                               # http://localhost:3000

Smoke test:

# Visit http://localhost:4000/graphql and run:
{ surveys { id name category } }

# Visit http://localhost:3000 — you should see 10 surveys.

# Run the provided e2e test
# - survey.e2e-spec.ts should PASS
# - submission.e2e-spec.ts should FAIL (your code will fix this)
cd api && npm test

Your task

Backend (~90 min)

1. Schema

Add a submissions table. A submission has:

Column Type Notes
id text PK use generateId()
survey_id text NOT NULL FK to surveys(id)
team_member text NOT NULL
due_date date NOT NULL
finalized_at timestamptz NULL null = open draft
created_at timestamptz NOT NULL default now()

Invariant: a survey can have many submissions over time, but only one open draft per (survey, team_member) at a time (finalized_at IS NULL). Enforce this at the DB level. (Hint: partial unique index.)

Add the table to src/core/drizzle/schema.ts AND write a new migration file in src/core/drizzle/migrations/ (e.g. 0001_submissions.sql).

2. GraphQL

Following the shape of Survey:

  • Submission type with: id, survey: Survey, teamMember, dueDate, finalizedAt.
  • Query: submissions(surveyId: ID): [Submission!]!
  • Mutation: startSubmission(input: StartSubmissionInput!): Submission where StartSubmissionInput = { surveyId, teamMember, dueDate }.
  • Mutation: finalizeSubmission(submissionId: ID!): Submission.
  • Field on Survey: activeDraft: Submission (the current open draft for the requesting team_member — for this take-home, treat the team_member as if there were only one; resolve "the" open draft, if any). Add it as a @ResolveField on a new SurveyFieldsResolver, OR extend SurveyResolver — your call.

Wire SubmissionModule to AppModule (it's already imported as an empty stub).

3. Hook

When a submission is started, fire a SubmissionStartedHook. Register a handler that writes a row to audit_events:

  • kind: 'submission.started'
  • payload: jsonb, { submissionId, surveyId, teamMember }

This handler must run inside the mutation transaction. The provided Hooks service + DrizzleTransactionalMutationsInterceptor make this work — but the rollback test in step 4 will verify it. If your handler doesn't participate in the tx, that test fails.

4. Tests (provided — this is your spec)

api/test/submission.e2e-spec.ts ships with this scaffold. Your code makes it pass. The tests are the executable contract for the GraphQL surface and the invariants — if you're unsure what behavior we expect, read the test.

The suite covers:

  • Happy path: startSubmission writes the audit row with the right kind and payload.
  • Invariant: a second open draft for the same (survey, team_member) is blocked.
  • Invariant: separate team members can each have open drafts on the same survey.
  • Invariant: a new draft for the same (survey, team_member) is allowed once the previous is finalized.
  • Finalize: finalizeSubmission sets finalizedAt.
  • Rollback: if the audit write fails, the submission insert is rolled back.

api/test/survey.e2e-spec.ts is shipped as a working reference for the Survey triad — it's not part of your task.

You aren't expected to add tests. Write extras locally for your own verification if useful; only the shipped tests are evaluated.

Frontend (~45 min)

On web/src/app/surveys/[id]/page.tsx, replace the TODO placeholder:

  • Show the survey details and its current activeDraft (if any).
  • If no active draft: form with team member + due date → calls startSubmission.
  • If active draft: show team member + due date + "Finalize" button → calls finalizeSubmission.

Don't over-style. Tailwind is wired; functional is enough.


How we'll review

  • Layering discipline — repo stays pure (no business logic, no hooks). Service orchestrates. Resolver is thin.
  • Transactional reasoning — did your hook handler genuinely run in the same tx? How did you verify? (We will run your e2e test.)
  • DB modeling — how did you enforce "one open draft per (survey, team_member)"? Did you index the FK column? (Postgres doesn't auto-index FKs.)
  • Type safety — no any. Repo returns UnsecuredDto<Submission>; service returns Submission.
  • AI usage transparency — see below.

Notes for review (required)

At the bottom of the README in your submission (or as AI_NOTES.md), tell us briefly:

  • What you used AI for.
  • Where you accepted its output as-is.
  • Where you pushed back, rewrote, or rejected its suggestions.
  • Anything you noticed but deferred — inefficiencies, scale concerns, design choices that nagged at you. 2-3 specific things with the tradeoff that kept you from fixing them.

We're hiring senior devs who can leverage AI critically and reason about the tradeoffs of their own code — both are positive signals, not gotchas.

Time budget

Target 2–4 hours. If you hit 4 hours and aren't done, stop and write up what you'd do next. Submitting an incomplete-but-coherent solution with a clear "here's what I'd do next" beats a sprawling unfinished one.


Code map

api/
├── src/
│   ├── main.ts                                  # Fastify bootstrap
│   ├── app.module.ts                            # registers GraphQL + interceptor + modules
│   ├── core/
│   │   ├── drizzle/
│   │   │   ├── schema.ts                        # surveys + audit_events. Add submissions here.
│   │   │   ├── drizzle.service.ts               # `tx` getter — use this in repos
│   │   │   ├── transactional-mutations.interceptor.ts
│   │   │   └── migrations/0000_init.sql
│   │   ├── hooks/
│   │   │   ├── hooks.service.ts                 # @OnHook discovery + run()
│   │   │   └── on-hook.decorator.ts
│   │   └── common/
│   │       ├── id.ts                            # generateId(): nanoid
│   │       └── unsecured-dto.ts
│   └── components/
│       ├── survey/                              # 🟢 reference pattern (read this first)
│       │   ├── dto/{survey,create-survey}.dto.ts
│       │   ├── survey.repository.ts
│       │   ├── survey.service.ts
│       │   ├── survey.resolver.ts
│       │   └── survey.module.ts
│       └── submission/                          # 🔴 your work
│           └── submission.module.ts             # empty stub
└── test/
    ├── survey.e2e-spec.ts                       # 🟢 reference e2e (Survey, passes)
    └── submission.e2e-spec.ts                   # 🔴 your spec — make this pass

web/
└── src/
    ├── app/
    │   ├── page.tsx                             # 🟢 surveys list (works)
    │   ├── surveys/[id]/page.tsx                # 🔴 your work below the TODO
    │   └── layout.tsx
    └── lib/
        ├── apollo.ts
        └── apollo-provider.tsx

Submission

Push to a fresh GitHub repo (or whatever your interviewer asked for) and send the link. We expect to see your commit history — small commits are fine, one giant commit is harder to review.

Good luck. Have fun with it.

About

Senior SWE Take Home

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages