REST API for StellarTickets — Secure. Verifiable. Powered by Stellar.
Built with NestJS + Prisma
(PostgreSQL). This service owns organizer/event metadata, authentication, and
the marketplace search surface — it never custodies ticket ownership itself.
The ticketing Soroban
contract is the source of truth for who owns a ticket and whether it's valid;
this API reads and writes through it, and keeps its own Postgres copy only as
a fast, searchable cache.
- New to this stack? Start here
- Non-custodial by design
- How this fits with the other repos
- Domain model
- Modules
- API reference
- Getting started
- Environment
- Testing
- Project structure
- More documentation
A plain-language glossary for anyone new to NestJS, Prisma, or the Stellar-specific pieces. Skip this if you already know the stack.
| Term | What it means | Why it matters here |
|---|---|---|
| NestJS | A TypeScript backend framework built around modules, controllers, and services (dependency-injected, à la Angular's structure but for the server). | Every feature area (auth, events, tickets, organizations, users, stellar) is its own Nest module — see Modules below. |
| Controller | The class that maps HTTP routes (@Get, @Post, …) to method calls. Has no business logic itself. |
tickets.controller.ts is only routing; the actual issue/transfer/check-in logic lives in tickets.service.ts. |
| Service | The class that holds business logic, injected into controllers (and other services) by Nest's DI container. | stellar.service.ts is the one service every other domain service calls through to touch the blockchain. |
| DTO (Data Transfer Object) | A plain class describing the shape of an incoming request body, decorated with class-validator rules. |
Combined with the global ValidationPipe in main.ts, a malformed request body is rejected before it reaches any service code. |
| Guard | Code that runs before a route handler and can block the request (return false/throw) — used here for JWT auth and role checks. |
JwtAuthGuard and RolesGuard in src/auth/guards gate every endpoint that requires a signed-in user or a specific role. |
| Prisma | A TypeScript ORM: you describe your schema in prisma/schema.prisma, and it generates a fully-typed database client plus SQL migration files. |
prisma/schema.prisma in this repo is the single source of truth for the Postgres schema; PrismaService wraps the generated client as an injectable Nest provider. |
| Migration | A versioned SQL file (generated by prisma migrate dev) that changes the database schema and is checked into git, so every environment's schema history is reproducible. |
prisma/migrations/20260803160811_init is the first one, in this repo. |
| JWT (JSON Web Token) | A signed token proving "this request came from an already-authenticated user" without a database lookup on every request. | Issued by POST /auth/login; JwtAuthGuard verifies it on protected routes using JWT_SECRET. |
| XDR | Stellar's binary transaction format. See the blockchain repo's glossary for the full picture. | This API's "build" endpoints return unsigned XDR; its "confirm" endpoints accept signed XDR back. |
| Non-custodial | This service never holds a private key that could move a user's funds or sign on their behalf. | See the next section — it's the single most important architectural decision in this repo. |
| Source account vs. signer | A Stellar transaction has a "source account" (whose sequence number/fee it uses) which is not necessarily the account that must authorize the operations inside it. | PLATFORM_SIGNER_SECRET is only ever used as a disposable source account for read-only simulations — never to sign a state-changing write. |
This backend never holds a user's Stellar secret key. Every on-chain action (publishing an event, issuing/purchasing/transferring/checking in/revoking/ reselling a ticket) is a two-step flow:
POST /.../<action>— the API simulates the contract call against the caller's own public key and returns an unsigned, fee-prepared XDR envelope.- The caller's wallet (Freighter, etc.) signs it client-side — the private key never leaves the browser extension, let alone reaches this server.
POST /.../confirm-<action>— the API relays the signed envelope to Soroban RPC, polls it to completion, and updates its own read-model (Ticket.status,Event.status, …) to match what's now true on-chain.
PLATFORM_SIGNER_SECRET is the one Stellar key this service does hold, and
it is deliberately limited: it's used only as a disposable source account for
read-only simulations (verify_ticket, get_event) that don't need any
particular signer's authorization. It never signs a transaction that changes
state.
See src/stellar/stellar.service.ts for
the implementation and
src/tickets/tickets.service.ts for how
each ticket action wires build → sign (client-side) → confirm together.
┌───────────────────┐ build-* ┌──────────────────────┐ submit signed XDR ┌────────────────────┐
│ frontend │◀──────────│ this repo │────────────────────▶│ blockchain │
│ (Next.js, browser) │ │ (NestJS + Postgres) │ │ `ticketing` contract │
│ │──────────▶│ │◀─── read state ──────│ (Stellar network) │
└──────────────────────┘ confirm-* └──────────────────────┘ └────────────────────┘
The frontend never talks to Soroban directly — it only ever calls this API, which owns the build/confirm XDR flow and the Postgres cache that makes marketplace search and dashboard listing fast without a chain read on every page load.
One flexible schema covers all twelve supported industries (concerts,
flights, sports, festivals, conferences, bus, movie theaters, museums,
tourist attractions, public transport, universities, corporate events) — the
Industry enum is the only industry-specific piece, used for filtering and
display copy. See prisma/schema.prisma for the full
schema with comments; the shape in brief:
User ──┬── memberships ──▶ OrganizationMember ◀── Organization
├── tickets ───────▶ Ticket
└── resaleListings ▶ ResaleListing
Organization ── events ──▶ Event ──┬── ticketTypes ──▶ TicketType
└── tickets ───────▶ Ticket ── resaleListings ▶ ResaleListing
A few fields worth calling out:
Event.chainEventId/Ticket.chainTicketId— theu64IDs returned by the contract'screate_event/issue_ticket.nulluntil the on-chain call succeeds, which is how the API knows an event/ticket exists in the database but hasn't actually been published/minted yet.Ticket.status— a cached projection of the contract's on-chainTicketStatus. The contract remains the source of truth; this column exists purely so listing/search queries don't need a Soroban RPC round trip. It's reconciled on every write path and by a periodic reconciliation job.Ticket.qrSecret— an opaque per-ticket secret embedded in the scannable QR code at/verify. Check-in validates this against both the database and the on-chain owner/status, so a photographed QR code alone can't be replayed as a valid entry.
| Module | Responsibility |
|---|---|
auth |
Registration/login, JWT issuance, password hashing (bcrypt) |
organizations |
Organizer accounts, membership roles (owner/admin/staff), the Stellar account that signs on-chain writes |
events |
Event/ticket-type CRUD, publishing an event on-chain |
tickets |
Issuance, primary sale, transfer, check-in, revocation, resale marketplace |
users |
Profile lookup, linking a Stellar public key to an account |
stellar |
The Soroban ticketing contract client — every module above calls through it for on-chain reads/writes |
prisma |
Wraps PrismaClient as an injectable PrismaService/PrismaModule |
common |
Shared decorators, e.g. a Stellar public-key validator for DTOs |
config |
Typed environment variable validation at boot |
All routes are prefixed with the app's base path; auth routes are public,
everything else requires a valid JWT (Authorization: Bearer <token>) unless
noted. "build" endpoints return unsigned XDR for the caller's wallet to sign;
the matching "confirm" endpoint accepts the signed XDR back.
Auth — src/auth
| Method & path | Purpose |
|---|---|
POST /auth/register |
Create an account (email, password, name) |
POST /auth/login |
Exchange credentials for a JWT |
Users — src/users
| Method & path | Purpose |
|---|---|
GET /users/me |
Current user's profile |
PATCH /users/me/wallet |
Link/update the caller's Stellar public key |
GET /users/lookup |
Look up a user (e.g. by email) for transfers |
Organizations — src/organizations
| Method & path | Purpose |
|---|---|
POST /organizations |
Create an organization |
GET /organizations/mine |
Organizations the caller is a member of |
GET /organizations/:id |
Organization detail |
Events — src/events
| Method & path | Purpose |
|---|---|
GET /events |
Public marketplace listing |
GET /events/:eventId |
Event detail |
GET /organizations/:organizationId/events |
Events under an organization |
POST /organizations/:organizationId/events |
Create a draft event |
POST /events/:eventId/ticket-types |
Add a ticket type (name, price, quantity) to a draft event |
POST /events/:eventId/publish |
build — unsigned XDR for the on-chain create_event call |
POST /events/:eventId/confirm-publish |
confirm — submits the signed XDR, sets chainEventId and status: PUBLISHED |
Tickets — src/tickets
| Method & path | Purpose |
|---|---|
GET /tickets/mine |
Tickets the caller owns |
GET /tickets/resale |
Active resale listings (marketplace) |
GET /tickets/verify/:qrSecret |
Look up a ticket by its QR secret, for the /verify gate-scanner flow |
POST /tickets/issue / confirm-issue |
Organizer-authorized issuance (off-chain payment already settled) |
POST /tickets/purchase / confirm-purchase |
Fully on-chain primary sale |
POST /tickets/:ticketId/transfer / confirm-transfer |
Direct transfer to another user |
POST /tickets/:ticketId/check-in / confirm-check-in |
Mark used at the gate |
POST /tickets/:ticketId/revoke / confirm-revoke |
Organizer voids a ticket |
POST /tickets/:ticketId/list-resale / confirm-list-resale |
List for resale (price capped by the event's anti-scalping policy on-chain) |
POST /tickets/:ticketId/cancel-resale / confirm-cancel-resale |
Pull a listing |
POST /tickets/:ticketId/buy-resale / confirm-buy-resale |
Buy a listed ticket; royalty + seller payout settle atomically on-chain |
See docs/API.md for full request/response shapes.
Prerequisites: Node.js ≥ 22 (see .nvmrc), a PostgreSQL
instance, and a deployed instance of the
ticketing contract (use its
testnet deployment walkthrough if you don't have one yet).
git clone https://github.com/StellarTickets/backend.git
cd backend
npm install
cp .env.example .env # fill in DATABASE_URL, JWT_SECRET, Soroban RPC config
npx prisma migrate dev # creates the database schema
npm run start:dev # http://localhost:3000, hot-reloadingOr with Docker (brings up Postgres alongside the app):
docker compose upSee .env.example for the full list. The Stellar-specific
ones are worth calling out:
| Variable | Meaning |
|---|---|
SOROBAN_RPC_URL |
The Soroban RPC endpoint used to simulate and submit transactions (e.g. https://soroban-testnet.stellar.org) |
STELLAR_NETWORK |
testnet / futurenet / mainnet — must match the frontend's NEXT_PUBLIC_STELLAR_NETWORK and whatever network the user's wallet is set to |
TICKETING_CONTRACT_ID |
The deployed ticketing contract address (starts with C) |
PLATFORM_SIGNER_SECRET |
A disposable Stellar secret key used only as the source account for read-only simulations — never used to sign a write, and never a user's key (see Non-custodial by design) |
npm test # unit tests (Jest, colocated *.spec.ts files)
npm run test:cov # with coverage
npm run test:e2e # end-to-end, against test/jest-e2e.json
npm run lint # ESLint, --fix.
├── prisma
│ ├── schema.prisma # the schema — see Domain model above
│ └── migrations
├── src
│ ├── auth # register/login, JWT, guards
│ ├── organizations
│ ├── events
│ ├── tickets
│ ├── users
│ ├── stellar # the Soroban contract client (build/sign/submit)
│ ├── prisma # injectable PrismaService
│ ├── common # shared decorators
│ ├── config # env var validation
│ ├── app.module.ts
│ └── main.ts # bootstrap: helmet, CORS, global ValidationPipe
├── test # e2e suite
├── docs # architecture, auth, database, API, FAQ
├── Dockerfile / docker-compose.yml
└── README.md
The docs/ directory goes deeper on specific topics:
| Doc | Covers |
|---|---|
ARCHITECTURE.md |
How this API fits into the wider system |
API.md |
Full request/response reference |
AUTHENTICATION.md |
JWT flow, guards, roles |
DATABASE.md |
Schema design notes |
NON_CUSTODIAL.md |
The build/sign/submit flow in depth |
PRISMA_7_NOTE.md |
Why Prisma is pinned to 6.x, not 7 |
ERROR_HANDLING.md |
Error response shape and conventions |
VALIDATION.md |
DTO/class-validator conventions |
RATE_LIMITING.md |
Rate limiting approach |
OBSERVABILITY.md |
Logging and monitoring |
CORS.md |
CORS configuration |
DEPLOYMENT.md |
Production deployment notes |
TESTING.md |
Test suite conventions |
GLOSSARY.md |
Extended terminology |
FAQ.md |
Common questions |
See also CONTRIBUTING.md, SECURITY.md,
and CHANGELOG.md.