Add descriptions and group by category: Key storage, Auth, Collections, Features Made-with: Cursor
7.9 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.
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.
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.