AI client onboarding forms for creators. You describe a client's project, the AI asks up to 4 quick questions, then it builds a form and a share link. Your client fills the form and you get the answers in your account.
| Part | Choice | Why |
|---|---|---|
| Frontend + backend | Next.js 16 (React + TypeScript) | One project for the UI and the Node.js API routes. Deploys to Vercel Hobby with zero config. |
| Styling | Tailwind CSS 4 | Colours and fonts live in one file: src/app/globals.css. |
| Database | Postgres (Neon, free plan) via @neondatabase/serverless |
Plain SQL you can read and run in Neon's SQL Editor. The HTTP driver needs no connection pool, so it suits Vercel's serverless functions. |
| AI | Groq free tier, model openai/gpt-oss-20b |
Free to start, very fast (a form takes a few seconds), and costs about $0.10 per million tokens on the paid tier. |
| Auth | Email + password (bcrypt + signed cookie) | No third-party accounts needed. |
| Resend (optional, free tier) | Emails you when a client submits a form. |
The AI code works with any OpenAI-compatible provider. If you want to switch to something else later (for example Google Gemini's free tier), change AI_BASE_URL, AI_MODEL and AI_API_KEY. You don't need to change any code.
You need Node.js 20.9 or newer (check with node -v). Get it from https://nodejs.org if needed.
# 1. Open the folder in VS Code, then open the terminal (Ctrl + `)
npm install
# 2. Create your settings file
cp .env.example .env.local # Windows PowerShell: copy .env.example .env.localOpen .env.local and fill in:
- DATABASE_URL: Sign up at https://neon.tech and create a project. On the project dashboard, click Connect, make sure Connection pooling is on, and copy the connection string. It looks like
postgresql://user:password@ep-xxxx-pooler.region.aws.neon.tech/neondb?sslmode=require. It already includes SSL (sslmode=require), so you don't need a certificate file. - AUTH_SECRET: Run this and paste the output:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" - AI_API_KEY: Create a free key at https://console.groq.com/keys
- ADMIN_EMAILS (optional): your own email, so you can open the admin dashboard. See Admin dashboard.
Then:
# 3. Create the tables in your Neon database
npm run db:setup
# 4. Start the app
npm run devOpen http://localhost:3000, click Register, and generate your first form.
Prefer to do step 3 in the browser? In the Neon console, open SQL Editor, paste the whole of
db/schema.sql, and click Run. It's safe to run more than once.
- Push the project to a GitHub repository (
.env.localis ignored, so your secrets stay local). - On https://vercel.com, click Add New → Project and import the repo.
- Under Environment Variables, add the same values as your
.env.local, except:- Set
APP_URLto your live URL, for examplehttps://yourdomain.com. - Add
ADMIN_EMAILSso you can open/adminon the live site. - For
DATABASE_URL, either paste the same Neon pooled connection string, or let Vercel set it for you (see below).
- Set
- Click Deploy.
Alternative for DATABASE_URL: the Neon integration. In your Vercel project, open Storage (or the Marketplace), choose Neon, and connect your Neon project. Vercel then adds DATABASE_URL (plus a few other PG* variables the app ignores) to your environment variables automatically. Redeploy afterwards so the new variable is picked up. If you use this, don't also add DATABASE_URL by hand.
The tables only need to be created once per database. If Vercel points at the same Neon database you used locally, you're done. If it's a new database, run db/schema.sql in that database's Neon SQL Editor.
- In Vercel, open your project, then Settings → Domains, and add
yourdomain.comandwww.yourdomain.com. - In Namecheap, go to Domain List → Manage → Advanced DNS and add the records Vercel shows you. Usually that's an
Arecord@ → 76.76.21.21and aCNAMErecordwww → cname.vercel-dns.com. - DNS changes can take anywhere from a few minutes to a few hours to go live.
Without RESEND_API_KEY, the app skips emails. You still get notifications in the app (the bell icon).
- Sign up at https://resend.com and create an API key. Put it in
RESEND_API_KEY. - In Resend, go to Domains → Add domain and add your domain. Then add the DNS records Resend shows you in Namecheap's Advanced DNS.
- Once the domain is verified, set
EMAIL_FROM=Formtified <notifications@yourdomain.com>.
Until the domain is verified, Resend's test sender can only email the address you signed up to Resend with.
Clients can attach files to a File upload question (up to 5 files, 10 MB each). Files go from the client's browser straight to Vercel Blob as private files; only the form's owner can open them, through /api/files/<id>.
- In Vercel, open your project → Storage → Create → Blob, name it (for example
formtified-files) and connect it to the project. Vercel addsBLOB_READ_WRITE_TOKENfor you. - Redeploy.
- To test uploads on your computer, copy
BLOB_READ_WRITE_TOKENfrom the Blob store's .env.local tab into your.env.local.
Without the token, file questions show "uploads aren't available" and never stop a client from submitting.
Daily clean-up. If a client uploads a file but never submits, the file is deleted the next day by /api/cron/cleanup (scheduled in vercel.json). Add a CRON_SECRET environment variable in Vercel (any long random string) so only Vercel can run it.
Credit packs are sold on Paystack's checkout page. Prices live in src/lib/credits.ts.
- Sign up at https://paystack.com. In Settings → API Keys & Webhooks, copy the Test Secret Key (
sk_test_...) intoPAYSTACK_SECRET_KEYin.env.localand Vercel. - On the same page, set Test Webhook URL to
https://yourdomain.com/api/paystack/webhook. - Buy a pack with one of Paystack's test cards to check it works.
- When Paystack has approved your business, switch both to the Live key and Live Webhook URL, and redeploy.
Credits are added only after Formtified checks with Paystack's API that the payment succeeded and the amount is right, and each payment adds credits once, even if the webhook and the return page both arrive. Without the key, the Credits page shows "coming soon".
A private area at /admin for running the platform.
Give yourself access
- Sign up in the app normally with your email.
- Add that email to
ADMIN_EMAILSin.env.local(and in Vercel's Environment Variables). For several admins, separate the emails with commas:ADMIN_EMAILS=you@yourdomain.com,partner@yourdomain.com. - Restart
npm run dev(on Vercel: redeploy). An Admin link appears in the sidebar, and an Admin dashboard button appears on the Account page on mobile.
Everyone else gets a "page not found" at /admin, so the area stays hidden.
What's inside
| Page | What you can do |
|---|---|
| Overview | Users, active users, paid subscribers, visitors today, forms, responses, AI usage, charts of signups and visitors, newest users, system status and database size |
| Site visits | Visitors and page views over 7/30/90 days, top pages, where visitors come from, countries, devices, browsers, and who's online now |
| Users | Search, filter by plan or status, sort, export to CSV. Open a user to add or remove credits, change their plan, suspend or delete them, or send them a message |
| Subscribers | Users on a paid plan, a breakdown by plan, and email opt-ins |
| Forms | Every form on the platform. Close, reopen or delete any of them |
| Notifications | Send an announcement to everyone, free users, paid subscribers, or one person. It appears in their notification bell, and optionally by email. Shows how many people have seen each one |
| Activity log | Every change made by an admin, with who did it and when |
How site visits are counted. Each page view sends a tiny request to /api/track. There are no cookies and no third-party tracker. A visitor is identified by a hash of the date, their IP address and their browser, which changes every day and can't be turned back into an IP. So "visitors" means unique visitors per day. Bots are ignored, and admin pages aren't counted. Countries come from Vercel, so they show as "Unknown" when running locally. In npm run dev, React runs effects twice, so local page views are counted double. That doesn't happen on the live site.
Suspending a user signs them out immediately and stops them signing in. Their forms stay online. To take a form down, close it from the Forms page.
Plans are free, starter, pro and studio (listed in src/lib/types.ts). Any plan except free counts as a paid subscriber. Buying a credit pack sets the plan to the biggest pack the user has bought; you can also change it by hand on a user's page.
- Two small calls per form. The first call returns up to 4 clarifying questions (capped at 900 tokens). The second returns the form (capped at 4,000 tokens). Users can skip the questions.
- Compact output. The AI replies in JSON with one-letter keys (
lfor label,ofor options, and so on). This cuts the output by about a third. The server then expands and validates it (src/lib/ai.ts,sanitizeFieldsinsrc/lib/forms.ts). - Short "thinking".
reasoning_effort: "low"keeps the model from spending tokens on reasoning. - Credits and rate limits. Each generated form costs 1 credit, and so does each AI brief of a response (saved, so viewing it again is free). The credit is refunded if the AI fails. There's also a per-user hourly cap (
AI_HOURLY_LIMIT). - Usage log. Every AI call is logged in the
ai_usagetable with its tokens and time. To see your spend:SELECT kind, COUNT(*), AVG(ms), SUM(prompt_tokens + completion_tokens) FROM ai_usage GROUP BY kind;
Speed: gpt-oss-20b on Groq runs at roughly 1,000 tokens per second, so most forms finish in a few seconds, well under the 30-second target.
Model changes: Groq retires models from time to time. If generation starts failing with a model error, check https://console.groq.com/docs/models and update AI_MODEL.
db/schema.sql All tables (run in Neon's SQL Editor or with npm run db:setup)
scripts/setup-db.mjs Runs schema.sql against DATABASE_URL
src/lib/
db.ts Neon Postgres queries over HTTP (no pool needed)
auth.ts Sign-in cookie, getUser / requireUser
ai.ts Prompts + AI calls (change prompts here)
forms.ts Form queries, link (slug) creation, field validation
templates.ts The free templates, also shown as public pages at /templates
credits.ts What things cost in credits, and the credit packs (prices)
paystack.ts Checkout, payment confirmation and webhook checks
files.ts, uploads.ts File-upload rules, and storing/deleting client files
email.ts Resend emails (single and batch)
admin.ts Every admin dashboard query and action
analytics.ts Cookie-free site visit tracking
src/app/
page.tsx Landing page
login/, signup/ Sign-in screen
(app)/ Signed-in area (sidebar on desktop, bottom nav on mobile)
dashboard/ generate/ forms/[id]/ responses/ new/ account/ credits/
f/[slug]/ The public form your clients fill
brief/[id]/ Printable brief of one response (Save as PDF)
docs/ Help Center; articles live in docs/articles.tsx
templates/ Public template library
admin/ Admin dashboard pages (only for ADMIN_EMAILS)
api/ Backend routes (auth, ai, forms, public submit, export, track, admin...)
src/components/ UI pieces (admin/ holds the dashboard's charts, tables and forms)
| Command | What it does |
|---|---|
npm run dev |
Start locally with hot reload |
npm run db:setup |
Create or update the tables |
npm run build |
Production build (run this before pushing to catch errors) |
npm run lint / npm run typecheck |
Check code quality and types |
DATABASE_URL is not set: The file must be named exactly.env.local. Restartnpm run devafter editing it.password authentication failed: The connection string was copied wrong or the password was reset. Copy it again from Neon's Connect button. If you reset the password in Neon, update.env.localand Vercel too.column "suspended_at" does not existorrelation "page_views" does not exist: Your database is from before the admin dashboard was added. Runnpm run db:setupagain. It only adds what's missing and doesn't touch your data./adminshows "page not found": Your email isn't inADMIN_EMAILS, or you didn't restartnpm run dev(or redeploy on Vercel) after adding it.relation "users" does not exist: The tables haven't been created yet. Runnpm run db:setup, or pastedb/schema.sqlinto Neon's SQL Editor.- The first request after a while is slow (1–2 seconds): That's normal on the free plan. Neon pauses an idle database after about 5 minutes and wakes it on the next query.
npm run db:setupsays DATABASE_URL is missing: Check that.env.localis in the project folder (not insidesrc/) and that the line starts withDATABASE_URL=with no spaces around=.- "AI is not configured": Add
AI_API_KEYand restart the server. - "The AI is busy right now" (429): You've hit Groq's free-tier rate limit. Wait a minute, or add billing on Groq.
- Your own branding on public forms.
- Voice-note answers, transcribed by AI.
- Conditional questions (show a question based on an earlier answer).