495289232be40ec84c4d1aca1f6b5cbff11fb8a6
Show longform image tags on posts, use shortened npubs for author names, and paginate the homepage meetups section with responsive load more. Co-authored-by: Cursor <cursoragent@cursor.com>
Belgian Bitcoin Embassy
A Nostr-powered community website for Belgium's monthly Bitcoin meetup.
Tech Stack
- Frontend: Next.js 14 (App Router), TypeScript, Tailwind CSS, Shadcn/ui
- Backend: Express.js, TypeScript, Prisma ORM
- Database: SQLite (default), PostgreSQL supported
- Auth: Nostr login (NIP-07), JWT sessions
Quick Start
1. Clone and install
# Install backend dependencies
cd backend
npm install
# Install frontend dependencies
cd ../frontend
npm install
2. Configure environment
# From project root
cp .env.example backend/.env
cp .env.example frontend/.env.local
Edit backend/.env with your SuperAdmin pubkeys (SUPERADMIN_PUBKEYS) and a secure JWT secret.
3. Set up database
cd backend
npx prisma generate
npx prisma db push
npx prisma db seed
4. Run development servers
# Terminal 1: Backend
cd backend
npm run dev
# Terminal 2: Frontend
cd frontend
npm run dev
- Frontend: http://localhost:3000
- Backend API: http://localhost:4000/api
Project Structure
/frontend Next.js application
/app App Router pages
/components React components
/lib Utilities and API client
/hooks Custom React hooks
/backend Express API server
/src Source code
/api Route handlers
/services Business logic
/middleware Auth middleware
/prisma Database schema and migrations
/context Design specs (reference only)
API Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/challenge | Get auth challenge |
| POST | /api/auth/verify | Verify Nostr signature |
| GET | /api/posts | List blog posts |
| GET | /api/posts/:slug | Get post by slug |
| POST | /api/posts/import | Import Nostr post |
| PATCH | /api/posts/:id | Update post |
| GET | /api/meetups | List meetups |
| POST | /api/meetups | Create meetup |
| PATCH | /api/meetups/:id | Update meetup |
| POST | /api/moderation/hide | Hide content |
| POST | /api/moderation/block | Block pubkey |
| GET | /api/auth/me | Current user's effective role and permissions |
| GET | /api/users | List users |
| PUT | /api/users/:pubkey/role | Assign a role to a user |
| GET | /api/admin/permissions | Permission registry (keys, labels, groups) |
| GET | /api/admin/roles | Each role and its permission set |
| PUT | /api/admin/roles/:role/permissions | Update a role's permissions |
| GET | /api/categories | List categories |
| POST | /api/categories | Create category |
Roles and permissions
Access is controlled by an ordered role hierarchy backed by a runtime-editable, granular permission system. From highest to lowest:
- SuperAdmin sourced only from the
SUPERADMIN_PUBKEYSenv var. Always has every permission. Cannot be created, edited, demoted, or removed through the UI. This replaces the old "admin set in env" concept. IfSUPERADMIN_PUBKEYSis unset, the app falls back to the legacyADMIN_PUBKEYSvariable. - Admin highest role assignable through the dashboard.
- Moderator
- Writer
- Guest the fallback for any logged-in pubkey with no assigned role. Guest is not a stored record, it is simply the absence of a role.
Each assignable role (Admin, Moderator, Writer) maps to a set of permission keys
stored in the RolePermission table. SuperAdmins manage these from the dashboard
Roles page, which renders a permission-by-role matrix. The dashboard nav and
actions are gated on the current user's resolved permissions, not on role names.
Safeguards
- SuperAdmin is never assignable through the UI or API. Only the env var grants it.
- A user cannot assign a role at or above their own, or modify a SuperAdmin.
- A user cannot grant a role a permission they do not themselves hold.
- A user cannot remove their own
users.assign_roleorroles.edit_permissions. SuperAdmin (env-sourced) is always the recovery path. - Unknown role values and permission keys are rejected by the server.
Adding a new permission key
- Add an entry to
PERMISSIONSin backend/src/constants/permissions.ts with akey,label, andgroup. - The key automatically appears in
GET /admin/permissionsand the Roles matrix page, so no UI changes are needed. - Apply it to the relevant endpoints with the
requires('<key>')guard. - Optionally grant it to roles by default in
DEFAULT_ROLE_PERMISSIONS(used by the seed) and in the migration so fresh and existing databases get it.
License
MIT
Languages
TypeScript
93.5%
HTML
4.6%
Shell
1.2%
JavaScript
0.6%