This adds typed asset APIs to the geo client, covers the 3D/image upload-share flow in integration tests, and introduces a simple Leaflet web demo that places objects on map features and manages sharing visibility via backend links. Made-with: Cursor
8.5 KiB
TypeScript Frontend Integration Guide
This document explains how frontend developers should integrate with the backend through the reusable TypeScript client at libs/geo-api-client.
See also: Frontend Development — demo app (
web/), local dev, build steps.Asset docs: Assets Storage and Sharing and Docker MinIO Local Development.
Primary backend URL for integration:
https://momswap.produktor.duckdns.org/
Deployment: API is proxied via reverse proxy from https://momswap.produktor.duckdns.org to backend at 172.17.0.1:8122. Docker Compose maps port 8122 for the reverse proxy.
Goals
- Keep cryptographic signing logic in one place.
- Avoid duplicating API request code in frontend apps.
- Use a consistent local key storage format across projects.
Client package location
- Source:
libs/geo-api-client/src - Entry point:
libs/geo-api-client/src/index.ts - Build output (browser ESM):
libs/geo-api-client/dist/index.js
Build and test the client
cd libs/geo-api-client
bun install
bun test # unit + integration tests (docs flow)
bun run build
Integration tests in test/integration.test.ts cover the recommended flow: register, login, create collection, create point feature, list features.
Public API (current)
Class: GeoApiClient
Source: GeoApiClient.ts
Constructor:
Key methods:
Key storage
ensureKeysInStorage()— Ensure a keypair exists; if none found, generate and save. Use on app init.getStoredKeys()— Get stored keypair without generating. Returns null if none.derivePublicKey(privateKey)— Derive public key from private key (Ed25519). Use when importing pk from backup/QR.importKeys(keys)— Overwrite stored keypair (e.g. after import or restore from QR).exportKeys()— Read stored keypair for export/backup.setAccessToken(token)— Set bearer token for authenticated requests. Call after login.
Auth
getServicePublicKey()— Fetch API service public key (for register-by-signature).createChallenge(publicKey)— Request login challenge; returns nonce and messageToSign.loginWithSignature(publicKey, privateKey)— Login via challenge-response. Returns bearer token; stores it internally.registerBySigningServiceKey(publicKey, privateKey)— Register without invitation by signing the API service key. 409 if already registered.createInvitation(payload, inviterPrivateKey)— Create invitation for new users. Inviter signs the payload.registerWithInvitation(...)— Register using an invitation. Proves key ownership and redeems invite.
Collections
listCollections()— List collections for the authenticated user.createCollection(name)— Create a new collection. Returns id and name.updateCollection(collectionId, name)— Rename a collection.deleteCollection(collectionId)— Delete a collection and its features.
Features
listFeatures(collectionId)— List GeoJSON features in a collection.createPointFeature(collectionId, lon, lat, properties)— Add a Point. lon ∈ [-180,180], lat ∈ [-90,90]. Returns feature id.deleteFeature(featureId)— Delete a feature.
Asset API integration note
Asset endpoints are currently available at backend API level (/v1/assets...) and can be called from frontend apps directly with authenticated fetch requests.
Current frontend contract points:
- Feature list responses include linked media under
feature.properties.assets. - Each asset includes a backend-relative download path (
link) like/v1/assets/{id}/download. - Frontend should use this relative path and avoid constructing direct S3 URLs.
Recommended integration flow
- Create one
GeoApiClientinstance per backend base URL. - Call
ensureKeysInStorage()when app initializes. - If not yet registered: call
registerBySigningServiceKey(publicKey, privateKey)(signs the API service key and publishes your public key). - Use
loginWithSignature()to obtain and set a bearer token. - Call collection/feature methods after authentication.
- Use
importKeys/exportKeysin profile settings UX.
Registration by signing service key
When SERVICE_PUBLIC_KEY (or ADMIN_PUBLIC_KEY) is set, users can register without an invitation:
GET /v1/service-key— fetch the server public key (clients use this for registration and further signed communication).- Sign that key with your private key.
POST /v1/auth/register-by-signaturewith{ publicKey, signature }.
Server keys are generated with ./bin/gen-server-keys.sh and stored in etc/.
Example (TypeScript app)
import { GeoApiClient } from "../libs/geo-api-client/dist/index.js";
const storage = window.localStorage;
const storageLike = {
getItem: (key: string) => storage.getItem(key),
setItem: (key: string, value: string) => storage.setItem(key, value),
removeItem: (key: string) => storage.removeItem(key),
};
const client = new GeoApiClient("https://momswap.produktor.duckdns.org", storageLike);
const keys = await client.ensureKeysInStorage();
// Register (ignored if already registered); then login
try {
await client.registerBySigningServiceKey(keys.publicKey, keys.privateKey);
} catch (_) {
// Already registered or registration disabled
}
await client.loginWithSignature(keys.publicKey, keys.privateKey);
const created = await client.createCollection("My Places");
await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" });
const features = await client.listFeatures(created.id);
console.log(features);
Security notes
- Private keys are currently stored in browser storage via the selected storage adapter.
- If your frontend has stronger security requirements, wrap the storage adapter with your own encryption/decryption layer before calling
setItem/getItem. - Never send private keys to the backend.
No-build frontend compatibility
For no-bundler apps, import the built ESM file:
<script type="module">
import { GeoApiClient } from "../libs/geo-api-client/dist/index.js";
// use client...
</script>
The backend itself serves static UI at /web/, but this library can be consumed by any frontend runtime that supports fetch, TextEncoder, and ES modules.
For local development you can switch the client base URL to http://localhost:8122.