Zindua
Next.js starter · v1.3.0

OTP login in 5 minutes

Scaffold a Next.js app with email and WhatsApp OTP, optional password auth, and optional Neon + Drizzle. @zindua/sdk runs on the server. Your API key never reaches the browser.

npx @zindua/create-app@latest my-app

One command

Scaffold with npx @zindua/create-app@latest my-app. Setup wizard runs in the browser after npm run dev.

Keys on server

ZINDUA_API_KEY lives in .env.local only. Route Handlers call the API, never the browser.

Email + WhatsApp

OTP channel (email or whatsapp) and template language are set in .env.local, not in the UI.

i18n routes

Localized pages at /en, /fr, /es with a locale switcher. UI language is separate from OTP template lang.

Zustand flow

Client auth state machine: credentials → otp → success. Server sessions via httpOnly cookie.

Neon + Drizzle

Optional Postgres for users, sessions, OTP codes, audit logs, and devices. Required for password auth.

Password auth

AUTH_MODE=otp | password | both. Register at /[locale]/register with scrypt-hashed passwords.

CLI maintenance

Switch UI with template, sync the auth engine with update, diagnose with doctor.

Quick start

From zero to localhost

Requires Node 18+ and a Zindua project API key.

npx @zindua/create-app@latest my-app
cd my-app
npm install
npm run dev

# Browser wizard at /setup — API key, channels, template, auth mode
# Restart dev server when setup finishes (Ctrl+C, npm run dev)

# Optional Neon after password auth in wizard:
# npm run db:push
Use npx, not npm i. The npm sidebar shows npm i @zindua/create-app for every package. That only installs the CLI into node_modules. Run npx @zindua/create-app@latest my-app to create a project folder with code.

SDK dependency: npm install @zindua/sdk (v1.2.7 included in the template).

Setup

Step-by-step

Configure Zindua first, then scaffold your app. Same template slug works for email and WhatsApp.

Step 01

Create a Zindua project

Sign up at zindua.run, create a project, and copy the API key from Dashboard → Projects. Use a test key locally, live key in production.

Step 02

Connect delivery channels

In your project: connect an email provider (Gmail, Outlook, SMTP…) and/or link WhatsApp via QR for OTP on both channels.

Step 03

Create templates

Slug otp-verification with {{code}} required. Optional {{appName}}. For login alerts, add new-login-alert with {{device}}, {{ip}}, {{time}}.

Step 04

Scaffold your app

Run npx @zindua/create-app@latest my-app. The CLI scaffolds the project only — no API key prompts in the terminal.

Step 05

Configure in the browser

cd my-app, npm install, npm run dev. Open localhost:3000 — the /setup wizard verifies your API key, picks OTP channel, UI template, auth mode, and optional Neon URL. Restart npm run dev when done.

Step 06

Optional: Neon database

If you chose password auth in the wizard, DATABASE_URL is already in .env.local. Run npm run db:push. Password mode requires DATABASE_URL.

Step 07

Test and deploy

Send OTP by email or WhatsApp, verify on /[locale]/verify, land on /[locale]/success. Deploy to Vercel or Railway with server-side env vars and a live API key.

Stack

Neon, Drizzle, Zustand

Optional Postgres persistence and a client-side auth state machine. Falls back to in-memory OTP when DATABASE_URL is unset.

Neon Postgres

Optional DATABASE_URL. Persists users, sessions, OTP codes, audit logs, and devices. Required for password register/login.

Drizzle schemas

lib/db/schema.ts defines app_users, app_sessions, app_otp_codes, app_audit_logs, app_devices. Push with npm run db:push.

Zustand state machine

store/auth-store.ts tracks credentials → otp → success on the client while the server owns sessions.

proxy.ts guard

Next.js 16 route guard protects /[locale]/success. httpOnly session cookie, rate limiting on send-otp.

Neon setup

# .env.local — optional unless AUTH_MODE=password|both
DATABASE_URL=postgresql://user:pass@host/db?sslmode=require

# Push Drizzle schema to Neon
npm run db:push

Drizzle tables

// lib/db/schema.ts (Drizzle + Neon)
app_users        — identifier, channel, password_hash
app_sessions     — token_hash, expires_at, ip, user_agent
app_otp_codes    — code_hash, attempts, expires_at
app_audit_logs   — action, ip, user_agent, metadata
app_devices      — fingerprint, label, last_seen_at

Zustand state machine

// store/auth-store.ts
type AuthStep = "credentials" | "otp" | "success";

// Client flow:
// 1. credentials — enter email/phone, call /api/auth/send-otp
// 2. otp         — enter 6-digit code, call /api/auth/verify-otp
// 3. success     — redirect to /[locale]/success
Auth

Auth modes

Set AUTH_MODE in .env.local. Password register/login requires DATABASE_URL (Neon).

AUTH_MODEBehavior
otpOTP only (default)
passwordEmail + password register and login
bothTabs on login: OTP and password

Password routes: Register UI at /[locale]/register. API: POST /api/auth/register, POST /api/auth/login-password.

Alerts

Login alert email

After OTP verify, register, or password login, optionally notify the user via a Zindua template.

# Post-login security alert (optional)
ZINDUA_LOGIN_ALERT_ENABLED=true
ZINDUA_LOGIN_ALERT_TEMPLATE=new-login-alert

# Create matching Zindua email template with:
# {{appName}}, {{device}}, {{ip}}, {{time}}
CLI

Commands for an existing project

Run inside your app folder after scaffolding.

Scaffold project

npx @zindua/create-app@latest my-app

Creates the app with placeholder .env.local. Run npm run dev — the setup wizard opens in your browser at /setup.

Switch UI

npx @zindua/create-app@latest template

Swap corporate or minimal layout. Does not overwrite .env.local.

Sync auth engine

npx @zindua/create-app@latest update

Pull latest routes, lib/, store/ from the package. Keeps .env.local untouched.

Diagnose setup

npx @zindua/create-app@latest doctor

Check .env.local, API key reachability, and common misconfigurations.

# Create project (recommended)
npx @zindua/create-app@latest my-app

# Same scaffold, optional folder name
npx @zindua/create-app@latest init my-app

# Inside an existing project
npx @zindua/create-app@latest template   # switch corporate | minimal UI
npx @zindua/create-app@latest update     # sync auth engine, keep .env.local
npx @zindua/create-app@latest doctor     # diagnose setup
Security

Security by default

End users only see your app. API keys and provider credentials stay in your server environment and Zindua dashboard.

Server-only API key

ZINDUA_API_KEY belongs in .env.local or your host's secret env vars. Never prefix it with NEXT_PUBLIC_.

Route Handlers only

@zindua/sdk runs in app/api/**/route.ts. Client components call your own /api/auth/* endpoints.

No secrets in git

.env.local is gitignored. Commit only .env.example with placeholder values.

Rotate if leaked

Regenerate the project API key in the dashboard immediately if a real key was exposed.

Environment variables

VariableNotes
ZINDUA_SETUP_COMPLETESet true by browser wizard when done (dev only)
ZINDUA_API_KEYrequiredFrom Dashboard → Projects
ZINDUA_APP_NAMEShown in templates as {{appName}}
ZINDUA_TEMPLATE_SLUGOTP template slug
ZINDUA_OTP_CHANNELemail or whatsapp (code config, not UI)
ZINDUA_OTP_LANGTemplate language for Zindua API
ZINDUA_DEFAULT_LOCALEUI locale: en, fr, es
AUTH_MODEotp | password | both (password needs DATABASE_URL)
ZINDUA_LOGIN_ALERT_ENABLEDSend new-login-alert email after sign-in
ZINDUA_LOGIN_ALERT_TEMPLATELogin alert template slug
APP_URLrequiredPublic app URL
AUTH_SESSION_SECRETrequiredLong random string for session signing
DATABASE_URLNeon Postgres; required for password auth; optional for persisted OTP/sessions
ZINDUA_API_BASE_URLOptional API override
// app/api/auth/send-otp/route.ts (server only)
import { getZindua } from "@/lib/zindua";

await getZindua().send({
  to: recipient,
  channel: "email", // or "whatsapp"
  template: "otp-verification",
  lang: "en",
  variables: { code: "482910", appName: "MyApp" },
});
Project

What you get

Next.js 16, Tailwind v4, corporate or minimal UI, i18n, Zustand, optional Drizzle + Neon. No SMTP.

my-app/
├── .env.example              # Placeholders only, safe to commit
├── .env.local                # Your real key, never commit
├── app/
│   ├── [locale]/
│   │   ├── page.tsx          # Login (OTP / password tabs)
│   │   ├── register/         # Password register (if AUTH_MODE allows)
│   │   ├── verify/           # Enter 6-digit OTP
│   │   └── success/          # Protected after login
│   ├── setup/                # Browser setup wizard (dev only)
│   └── api/
│       ├── setup/            # Verify API key, save .env.local
│       └── auth/
│           ├── send-otp/route.ts
│           ├── verify-otp/route.ts
│           ├── register/route.ts
│           ├── login-password/route.ts
│           └── logout/route.ts
├── lib/
│   ├── zindua.ts             # Server-only SDK client
│   ├── otp/                  # OTP store (memory or DB)
│   ├── db/schema.ts          # Drizzle tables for Neon
│   ├── auth/                 # sessions, password, login-alert
│   ├── i18n/                 # en, fr, es dictionaries
│   └── security.ts
├── store/auth-store.ts       # Zustand: credentials → otp → success
├── components/auth/          # corporate | minimal UI
├── proxy.ts                  # Guards /[locale]/success
└── drizzle.config.ts

WordPress for sites. Next.js for apps.

Same Zindua project, same templates. Pick the integration that fits your stack.

API reference →