Skip to content

Latest commit

 

History

195 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StellarTickets — Backend

REST API for StellarTicketsSecure. 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.

Table of contents

New to this stack? Start here

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.

Non-custodial by design

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:

  1. POST /.../<action> — the API simulates the contract call against the caller's own public key and returns an unsigned, fee-prepared XDR envelope.
  2. The caller's wallet (Freighter, etc.) signs it client-side — the private key never leaves the browser extension, let alone reaches this server.
  3. 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.

How this fits with the other repos

┌───────────────────┐  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.

Domain model

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 — the u64 IDs returned by the contract's create_event/issue_ticket. null until 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-chain TicketStatus. 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.

Modules

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

API reference

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.

Authsrc/auth

Method & path Purpose
POST /auth/register Create an account (email, password, name)
POST /auth/login Exchange credentials for a JWT

Userssrc/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

Organizationssrc/organizations

Method & path Purpose
POST /organizations Create an organization
GET /organizations/mine Organizations the caller is a member of
GET /organizations/:id Organization detail

Eventssrc/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

Ticketssrc/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.

Getting started

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-reloading

Or with Docker (brings up Postgres alongside the app):

docker compose up

Environment

See .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)

Testing

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

Project structure

.
├── 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

More documentation

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.

About

StellarTickets backend API (issuance, verification, transfers, resale marketplace) https://stellarticketsbackend.netlify.app/

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages