This updates developer docs and web demos to use backend upload endpoints, adds a client upload helper, and aligns integration tests with the no-direct-MinIO URL flow. Made-with: Cursor
7.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://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
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 backend upload URL, upload binary, 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 withproperties.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 backend upload endpoint (PUT /v1/assets/{id}/upload) -
uploadAssetBinary(assetId, payload, contentType?)— upload binary through backend endpoint -
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. linkis always backend-relative (for example/v1/assets/{id}/download).- Frontend should call
resolveRelativeLink(link)and must not build 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. - Create collection and map features.
- For media upload:
- compute file checksum
- call
createOrLinkAsset - call
uploadAssetBinary(or callgetAssetSignedUploadUrl+ manualfetch) - upload file to backend endpoint
- Render and share assets from
properties.assetslinks. - Use
setAssetVisibilityto toggle sharing. - 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: place and upload a 3D object (.glb)
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: <input id="modelFile" type="file" accept=".glb,.gltf" />
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<string> => {
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 through backend upload endpoint
await client.uploadAssetBinary(
asset.asset.id,
file,
asset.asset.mimeType || "application/octet-stream"
);
// 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:
<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.