An event management and community platform featuring event creation, community management, and social feeds.
Note: Zenao is currently a Web2 application, with plans to transition to Web3 using base.
- Tech Stack
- Prerequisites
- Quick Start - Full Local Development
- Environment Variables Reference
- Testing
- Clerk Authentication Setup
- File Uploads with Pinata
- Development Workflows
- Observability (Optional)
- Working with Staging/Production
- Troubleshooting
- Make Commands
- Project Structure
- Frontend: Next.js 15, React, TypeScript, TailwindCSS, Shadcn UI
- Backend: Go, Connect-RPC (gRPC-web)
- Database: SQLite/LibSQL (Turso), GORM, Atlas migrations
- Auth: Clerk
- Testing: Cypress (E2E)
- Observability: OpenTelemetry, Sentry
⚠️ Important: This project uses Node.js 20.13.1 (see.nvmrc), which is not the latest version. Using a different Node version may cause package-lock.json conflicts and CI failures. We strongly recommend using a Node version manager like nvm or fnm to switch to the correct version:nvm use # or: fnm use
Follow these steps to run the complete stack locally (frontend + backend + database):
Before you start: You may need to configure these services:
- Clerk Authentication Setup - If the default test keys have expired
- File Uploads with Pinata - Required to upload images (e.g., create events)
Run the setup script to install dependencies, configure environment, and initialize the database:
make setup-dev
Then start the development servers:
make dev
That's it! The app will be running at:
- Frontend: http://localhost:3000
- Backend: http://localhost:4242
⚠️ Note: The Go backend reads environment variables from your shell, not from.env.local. Export the Clerk secret key before runningmake devif you modified the default key:export ZENAO_CLERK_SECRET_KEY=sk_test_... # Must match CLERK_SECRET_KEY in .env.local
If you prefer to run each step manually:
nvm use # or fnm use
npm install
Create .env.local from the template:
cp .env.example .env.local
# Create and migrate the local SQLite database
make migrate-local
This creates a dev.db file in the project root.
In a dedicated terminal, export the Clerk secret key (if you modified the default) and start the backend:
export ZENAO_CLERK_SECRET_KEY=sk_test_... # Must match CLERK_SECRET_KEY in .env.local
go run ./backend start
The backend will run on http://localhost:4242
Optional: Generate fake data for development:
go run ./backend fakegen
In a new terminal:
npm run dev
- Frontend: http://localhost:3000
- Backend API: http://localhost:4242
- Database:
dev.db(SQLite file in project root)
You can inspect the database using any SQLite client:
sqlite3 dev.db
# Or use a GUI like DB Browser for SQLite
These variables are set by copying .env.example:
# Clerk Authentication (test keys for local development)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
CLERK_SECRET_KEY=""
# Backend Configuration
NEXT_PUBLIC_ZENAO_BACKEND_ENDPOINT=http://localhost:4242
NEXT_PUBLIC_ZENAO_NAMESPACE=zenao
# Stripe configuration
NEXT_PUBLIC_STRIPE_DASHBOARD_URL=https://dashboard.stripe.com/test
# File uploads - See README "File Uploads with Pinata" section
NEXT_PUBLIC_GATEWAY_URL=pinata.zenao.io
PINATA_GROUP=f2ecce4d-b615-48ee-8ae8-744145b40dcb # Optional: for organizing files in Pinata dashboard
PINATA_JWT="" # Required for uploading images (e.g., to create events)
# Observability (optional)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
SEOBOT_API_KEY=a8c58738-7b98-4597-b20a-0bb1c2fe5772
The backend reads these from the environment (prefixed with ZENAO_), not from the .env.local. All have defaults, so they're optional for local development:
⚠️ Important: If you modifyCLERK_SECRET_KEYin.env.local, you must also exportZENAO_CLERK_SECRET_KEYwith the same value before starting the backend.
# Optional overrides (backend has sensible defaults):
ZENAO_CLERK_SECRET_KEY=sk_test_... # Default: sk_test_...
ZENAO_DB=dev.db # Default: dev.db
ZENAO_ALLOWED_ORIGINS=* # Default: * (all origins)
ZENAO_MAIL_SENDER=contact@mail.zenao.io # Default: contact@mail.zenao.io
ZENAO_RESEND_SECRET_KEY= # Default: empty (emails disabled)
ZENAO_STRIPE_SECRET_KEY= # Default: empty (stripe disabled)
ZENAO_APP_BASE_URL= # Default: https://zenao.io/
DISCORD_TOKEN= # Default: empty (Discord disabled)
Run Go backend tests:
make test
Run E2E tests in headless mode (automated):
make test-e2e
Or run manually with the Cypress UI:
1. Setup environment:
cp .env.example .env.local
2. Start E2E infrastructure:
go run ./backend e2e-infra
This command sets up the E2E local environment:
- Applies Atlas migrations on a temporary SQLite DB (
e2e.db) - Generates fake data (users, events, posts, communities, etc.)
- Starts the backend
- Exposes
http://localhost:4243/resetendpoint for test automation (deduplicates/queues concurrent reset requests) - Prints logs from all services
Optional: Use the --ci flag to also build and start the Next.js frontend in the background (used for CI):
go run ./backend e2e-infra --ci
See backend/e2e_infra.go for implementation details.
3. Wait for stack readiness:
READY | ----------------------------
4. Start frontend (new terminal):
npm run dev
5. Open Cypress (new terminal):
npm run cypress:e2e
Select a test file (e.g., cypress/main.cy.ts) to run. Tests auto-rerun on file changes.
The project includes default Clerk test keys that work out of the box. If you encounter authentication errors (e.g., "unauthorized"), the test keys may have expired.
1. Create a free Clerk account: clerk.com/sign-up
2. Create a new application in the Clerk dashboard
3. Get your API keys:
- Dashboard → API Keys
- Copy
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY(starts withpk_test_...) - Copy
CLERK_SECRET_KEY(starts withsk_test_...)
4. Update .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
CLERK_SECRET_KEY=sk_test_your_key_here
5. Export for backend:
export ZENAO_CLERK_SECRET_KEY=sk_test_your_key_here
⚠️ Note: File uploads are required to create events. Without Pinata configured, you cannot upload event images.
1. Create a free Pinata account: app.pinata.cloud/register
2. Create an API Key:
- Dashboard → API Keys → + New Key
- Grant Files write permission and Group read permission
- Copy the JWT (shown only once!)
3. Create a Gateway:
- Dashboard → Gateways → + New Gateway
- Copy your gateway domain (e.g.,
your-name.mypinata.cloud)
4. (Optional) Create a Group:
- Dashboard → Groups → + Create Group
- Copy the Group ID (used to organize uploaded files)
5. Update .env.local:
PINATA_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
NEXT_PUBLIC_GATEWAY_URL=your-name.mypinata.cloud # Domain only, no https://
PINATA_GROUP=your-group-id # Optional
6. Restart: npm run dev
Upload fails:
- Check you haven't exceeded 1GB free tier limit
- Restart dev server after changing
.env.local
Edit .proto files in the api/ directories, then regenerate code:
make generate
This will:
- Run protobuf code generation
- Build email templates
- Format Go code
1. Edit GORM models in ./backend/gzdb (models must embed gorm.Model or have gorm annotations)
2. Update Atlas schema:
make update-schema
3. Create migration:
atlas migrate diff $MIGRATION_NAME \
--dir "file://migrations" \
--to "file://schema.hcl" \
--dev-url "sqlite://file?mode=memory"
4. Apply migration locally:
make migrate-local
5. Apply to staging/production:
# For staging or prod, set TURSO_TOKEN with a write-enabled token
export TURSO_TOKEN=<your-token>
atlas migrate apply --dir "file://migrations" --env staging # or prod
Note: Production tokens should be short-lived (1 day max).
For debugging and tracing requests across the stack, you can run an OpenTelemetry collector with Jaeger locally.
Start the full stack with OTEL enabled:
make dev-otel
This starts everything together:
- Frontend on http://localhost:3000
- Backend on http://localhost:4242
- OTEL Collector on port 4318
- Jaeger UI on http://localhost:16686
1. Start the OTEL stack:
docker compose -f dev.docker-compose.yml up
This starts:
- OTEL Collector on port 4318 - receives traces from the app
- Jaeger UI on http://localhost:16686 - visualize traces
2. Enable OTEL in your environment:
Set the OTEL endpoint in .env.local:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
3. Restart the backend to start sending traces.
4. Open Jaeger UI at http://localhost:16686 to view request traces.
This is useful for:
- Debugging slow requests
- Understanding request flow between frontend and backend
- Performance optimization
If you want to develop the frontend using the staging backend (instead of running the backend locally):
1. Get staging environment variables:
Copy the staging environment variables from your deployment platform's dashboard (Netlify, etc.) or ask a team admin for the .env.local file with staging credentials.
Note: You need to be a team member to access staging environment variables. Contact a team admin if you don't have access.
2. Start only the frontend:
npm run dev
Now your local frontend will connect to the staging backend and use real staging data.
When using staging environment variables:
- You'll see real user data from staging
- Authentication uses the staging Clerk instance
- Any changes affect the staging database
Use with caution - this is real data, not test data!
Ensure you've run migrations:
make migrate-local
The database is created at dev.db in the project root after running make migrate-local.
| Command | Description |
|---|---|
make setup-dev |
Install dependencies, configure environment, run migrations, and generate fake data |
make dev |
Start backend and frontend servers together |
make dev-otel |
Start backend, frontend, and OTEL stack (Jaeger UI at localhost:16686) |
make test |
Run Go backend unit tests |
make test-e2e |
Run Cypress E2E tests in headless mode |
make generate |
Regenerate protobuf code and email templates |
make migrate-local |
Apply database migrations to local dev.db |
make update-schema |
Update Atlas schema from GORM models |
make lint-fix |
Run ESLint with auto-fix |
├── app/ # Next.js app router pages
├── backend/ # Go backend (Connect-RPC handlers)
├── components/ # React components
├── cypress/ # E2E tests
├── migrations/ # Atlas database migrations
├── api/ # Protobuf definitions
├── lib/ # Shared utilities
├── public/ # Static assets
├── dev.db # Local SQLite database (created after setup)
└── .env.local # Environment variables (create from .env.example)