The backend API and on-chain indexer for GistPin. This service is the bridge between the web client and both the Stellar/Soroban blockchain and the Postgres database.
- Indexes on-chain events from the
GistRegistrySoroban contract - Stores enriched gist data in Postgres + PostGIS for fast geospatial queries
- Exposes a REST API consumed by the GistPin frontend
- Bridges to IPFS/Pinata for full gist content storage (the chain only holds a hash)
| Layer | Choice |
|---|---|
| Language | TypeScript |
| Runtime | Node.js >= 20 |
| Framework | NestJS |
| Database | PostgreSQL 15 + PostGIS extension |
| ORM / Query | TypeORM (with PostGIS support) |
| Blockchain | Stellar Horizon + Soroban RPC |
| Storage bridge | IPFS via Pinata (or self-hosted node) |
| Config | @nestjs/config with typed configuration |
| Testing | Jest (built into NestJS) |
Backend/
├── src/
│ ├── main.ts # App bootstrap
│ ├── app.module.ts # Root module
│ ├── config/
│ │ └── configuration.ts # Typed config via @nestjs/config
│ ├── gists/ # Gist feature module
│ │ ├── gists.module.ts
│ │ ├── gists.controller.ts # Route handlers
│ │ ├── gists.service.ts # Business logic
│ │ ├── dto/
│ │ │ ├── create-gist.dto.ts
│ │ │ └── query-gists.dto.ts
│ │ └── entities/
│ │ └── gist.entity.ts
│ ├── indexer/ # Soroban event watcher
│ │ ├── indexer.module.ts
│ │ └── indexer.service.ts
│ ├── soroban/ # Soroban RPC client wrapper
│ │ ├── soroban.module.ts
│ │ └── soroban.service.ts
│ ├── ipfs/ # IPFS pinning service
│ │ ├── ipfs.module.ts
│ │ └── ipfs.service.ts
│ └── geo/ # Geospatial helpers (geohash encoding)
│ └── geo.service.ts
├── test/ # e2e tests
├── .env.example
├── package.json
└── README.md
- Node.js >= 20 — nodejs.org
- PostgreSQL 15 with the PostGIS extension
- npm (comes with Node.js)
Why PostGIS? The core feature of GistPin is querying gists by distance — "show me everything within 500m of these coordinates." PostGIS adds a spatial index that makes this instant, even at scale.
git clone https://github.com/PinSpace-Org/GistPin.git
cd GistPin/Backend
npm installOption A — Docker (quickest)
docker run -d \
--name gistpin-db \
-e POSTGRES_USER=gist \
-e POSTGRES_PASSWORD=gist \
-e POSTGRES_DB=gist \
-p 5432:5432 \
postgis/postgis:15-3.3Option B — Homebrew (macOS)
brew install postgresql@15 postgis
brew services start postgresql@15
psql -U postgres -c "CREATE USER gist WITH PASSWORD 'gist';"
psql -U postgres -c "CREATE DATABASE gist OWNER gist;"
psql -U gist -d gist -c "CREATE EXTENSION postgis;"cp .env.example .envFill in the values — minimum required for local dev are the DATABASE_* fields.
npm run start:devAPI available at: http://localhost:3000
GET /health
Returns { "status": "ok" }.
GET /gists?lat=5.6037&lon=-0.1870&radius=500&limit=20&cursor=
| Param | Type | Default | Description |
|---|---|---|---|
lat |
number | required | Latitude |
lon |
number | required | Longitude |
radius |
number | 500 |
Radius in metres (max 5000) |
limit |
number | 20 |
Max results (max 100) |
cursor |
string | — | Pagination cursor |
POST /gists
Content-Type: application/json
{
"lat": 5.6037,
"lon": -0.1870,
"text": "Great street food here tonight",
"authorAddress": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}authorAddress is optional — anonymous posting is fully supported.
What happens internally:
- Validate + sanitise input
- Pin content to IPFS → receive CID
- Derive
locationCellfrom(lat, lon)via geohash - Submit
post_gist(author, locationCell, contentHash)to Soroban - Persist the record in Postgres
- Return the created gist
Table: gists
| Column | Type | Notes |
|---|---|---|
id |
uuid PK |
Internal primary key |
gist_id |
bigint UNIQUE |
On-chain ID from GistRegistry contract |
location_cell |
text |
Coarse geohash cell |
location |
geography(Point, 4326) |
PostGIS point for geo queries |
lat |
float8 |
Stored for convenience |
lon |
float8 |
Stored for convenience |
content_cid |
text |
IPFS CID |
text |
text |
Full gist text (cached from IPFS) |
author_address |
text |
Nullable — anonymous posts allowed |
tx_hash |
text |
Stellar transaction hash |
created_at |
timestamptz |
src/indexer/indexer.service.ts runs as a background NestJS worker. On startup it polls the Soroban RPC for new GistRegistry contract events and upserts each into Postgres. This keeps the DB in sync with on-chain state — gists posted directly on-chain still appear in query results.
| Command | Description |
|---|---|
npm run start:dev |
Start with hot-reload |
npm run build |
Compile TypeScript |
npm run start:prod |
Run compiled output |
npm test |
Unit tests |
npm run test:e2e |
End-to-end tests |
- Keep business logic in
services/, keep controllers thin (validate + delegate only). - Breaking API changes must be opened as an issue before implementation.
- All new behaviour should come with a unit or e2e test.