# 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](frontend-development.md) — demo app (`web/`), local dev, build steps.
>
> **Asset docs:** [Assets Storage and Sharing](assets-storage-and-sharing.md) and [Docker MinIO Local Development](docker-minio-local-dev.md).
Primary backend URL for integration:
- `https://tenerife.baby/`
Deployment: API is proxied via reverse proxy from `https://tenerife.baby` 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
```bash
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 both:
- Base flow: register, login, collection and feature CRUD
- Asset flow: create/link asset, request signed upload URL, and toggle visibility
## Public API (current)
### Class: `GeoApiClient`
Source: `libs/geo-api-client/src/GeoApiClient.ts`
Constructor:
- `new GeoApiClient(baseUrl, storage, storageKey?)`
Key methods:
- **Key storage**
- `ensureKeysInStorage()`
- `getStoredKeys()`
- `derivePublicKey(privateKey)`
- `importKeys(keys)`
- `exportKeys()`
- `setAccessToken(token)`
- **Auth**
- `getServicePublicKey()`
- `createChallenge(publicKey)`
- `loginWithSignature(publicKey, privateKey)`
- `registerBySigningServiceKey(publicKey, privateKey)`
- `createInvitation(payload, inviterPrivateKey)`
- `registerWithInvitation(...)`
- **Collections and features**
- `listCollections()`
- `createCollection(name)`
- `updateCollection(collectionId, name)`
- `deleteCollection(collectionId)`
- `listFeatures(collectionId)` (typed with `properties.assets`)
- `createPointFeature(collectionId, lon, lat, properties)`
- `deleteFeature(featureId)`
- **Assets (new)**
- `createOrLinkAsset({...})` — create metadata or reuse existing asset by checksum/ext and link it to a feature
- `getAssetSignedUploadUrl(assetId, contentType?)` — get signed `PUT` URL for binary upload
- `setAssetVisibility(assetId, isPublic)` — owner toggles public/private access
- `resolveRelativeLink(path)` — converts backend-relative asset links to absolute URLs for browser usage
## Asset frontend contract
- Feature responses include linked assets in `feature.properties.assets`.
- Asset item fields include: `id`, `kind`, `name`, `description`, `checksum`, `ext`, `isPublic`, `link`.
- `link` is always backend-relative (for example `/v1/assets/{id}/download`).
- Frontend should call `resolveRelativeLink(link)` and must not build direct S3 URLs.
## Recommended integration flow
1. Create one `GeoApiClient` instance per backend base URL.
2. Call `ensureKeysInStorage()` when app initializes.
3. If not yet registered: call `registerBySigningServiceKey(publicKey, privateKey)` (signs the API service key and publishes your public key).
4. Use `loginWithSignature()` to obtain and set a bearer token.
5. Create collection and map features.
6. For media upload:
- compute file checksum
- call `createOrLinkAsset`
- call `getAssetSignedUploadUrl`
- upload file to signed URL
7. Render and share assets from `properties.assets` links.
8. Use `setAssetVisibility` to toggle sharing.
9. Use `importKeys`/`exportKeys` in profile settings UX.
## Registration by signing service key
When `SERVICE_PUBLIC_KEY` (or `ADMIN_PUBLIC_KEY`) is set, users can register without an invitation:
1. `GET /v1/service-key` — fetch the server public key (clients use this for registration and further signed communication).
2. Sign that key with your private key.
3. `POST /v1/auth/register-by-signature` with `{ publicKey, signature }`.
Server keys are generated with `./bin/gen-server-keys.sh` and stored in `etc/`.
## Example: place and upload a 3D object (`.glb`)
```ts
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://tenerife.baby", 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");
const feature = await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" });
// Assume this comes from:
const fileInput = document.getElementById("modelFile") as HTMLInputElement;
const file = fileInput.files?.[0];
if (!file) throw new Error("Select a .glb/.gltf file first");
const toSha256Hex = async (f: File): Promise => {
const buffer = await f.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
const checksum = await toSha256Hex(file);
const ext = file.name.toLowerCase().endsWith(".gltf") ? "gltf" : "glb";
// Create metadata (or reuse existing by checksum+ext) and link to feature
const asset = await client.createOrLinkAsset({
featureId: feature.id,
checksum,
ext,
kind: "3d",
mimeType: file.type || (ext === "gltf" ? "model/gltf+json" : "model/gltf-binary"),
sizeBytes: file.size,
name: "Palm Tree Model",
description: "3D object placed on map",
isPublic: true,
});
// Upload binary to object storage through signed URL
const signed = await client.getAssetSignedUploadUrl(asset.asset.id, asset.asset.mimeType);
await fetch(signed.url, {
method: signed.method,
headers: asset.asset.mimeType ? { "Content-Type": asset.asset.mimeType } : undefined,
body: file,
});
// Read shareable relative link from feature payload
const features = await client.listFeatures(created.id);
const firstAsset = features.features[0]?.properties?.assets?.[0];
if (firstAsset) {
const shareUrl = client.resolveRelativeLink(firstAsset.link);
console.log("Share URL:", shareUrl);
}
// Optional: owner can disable public access later
await client.setAssetVisibility(asset.asset.id, false);
```
## 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:
```html
```
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`.