techne-sdk
TechneSDK for apps on the Techne platform: sign in, put, query, useEvent and the read-time check.
- npm: one package,
techne-sdk, with subpaths. - Rust: the crate
techne-sdk(crates/techne-sdk). It holds the logic that must give the platform's answers exactly: the check, the version ranking (eventState) andput's private-first, public-last plan. The npm package runs the same crate as WebAssembly, or as wasm2js on hosts without WebAssembly (Hermes). Opt-in features add the atproto plumbing services share (no platform crate either):cryptofor service-auth JWTs, signed labels, record proofs and service-account passwords, andnetfor DID resolution, verified record fetches and PDS password sessions.
The SDK depends on two things only: the published lexicons (the lexicons repo at a tag) and the platform's published API. It contains no platform code.
Install
Every import goes through the package's exports, so the resolver has to read them:
- TypeScript:
moduleResolutionnode16,nodenextorbundler. Under the oldnode(node10) mode the subpath types do not resolve. - React Native: Metro with package exports on (
unstable_enablePackageExports, the default from Expo SDK 53).techne-sdk/wasmloads the check throughtechne-sdk/wasm/load, whosereact-nativecondition picks the wasm2js build (Hermes has no WebAssembly); that file is ESM that calls Metro'srequire.
| Import | What it holds |
|---|---|
techne-sdk (also techne-sdk/generated) |
the generated XRPC client (AtpBaseClient) and every lexicon type |
techne-sdk/lexicons |
the generated lexicon dictionary (schemaDict, schemas, lexicons, ids): every lexicon document the client was built from, for reading descriptions at run time (large; keep it server side) |
techne-sdk/types/<nsid path> |
one lexicon's types, e.g. techne-sdk/types/social/scenius/getContactInvite |
techne-sdk/react |
useEvent(uri): one commons event or place as its versions, with the check's verdict |
techne-sdk/versions |
the same state without React: eventState, verifyAnswer, version labels |
techne-sdk/auth |
sign-in ("Sign in with Techne"): signIn, signUp, restore, signOut, clientMetadata, scopes (browser OAuth) |
techne-sdk/connect |
connect(session, platform) / connectHosted(did, fetchHandler): the clients over one session |
techne-sdk/put |
put(techne, records, { deletes }): the one write, deletes included |
techne-sdk/query |
the platform's generic queries: listRecords, getRecords, hosts |
techne-sdk/links |
link tokens: create, redeem, check, revoke |
techne-sdk/wasm |
the check itself: validate, decide, verify |
techne-sdk/catalog |
the browsable lexicon catalog |
techne-sdk/nsid, techne-sdk/nsid.json |
every NSID as a constant (NSID, METHOD) |
Datetime fields are plain strings: send Date.prototype.toISOString() (UTC, trailing Z).
Reading one event (the platform's getVersions is public, so an anonymous client will do):
import { AtpBaseClient } from 'techne-sdk'
import { useEvent } from 'techne-sdk/react'
const platform = new AtpBaseClient({ service: 'https://<platform host>' })
const load = async (uri: string) => (await platform.pub.techne.draft.commons.getVersions({ uri })).data
function Event({ uri }: { uri: string }) {
const { loading, error, fields } = useEvent(uri, { load })
if (loading) return <p>Loading</p>
if (error) return <p>Could not load it</p>
return <h1>{String(fields?.name?.value ?? '')}</h1>
}
Pass { claims } too, and verification holds the check's answer for every record (valid, signer, chain verified).
Sign in with Techne
techne-sdk/auth signs a person in from the browser. Each app is its own atproto OAuth client, the way "Sign in with Google" works: the app serves its client metadata at its client id, the person signs in at their own PDS, and the app keeps the session (an OAuthSession, with did and fetchHandler) that connect takes. It runs on @atproto/oauth-client-browser, loaded the first time it is used.
import { clientMetadata, restore, signIn, TECHNE_PLATFORM_DID } from 'techne-sdk/auth'
import { connect } from 'techne-sdk/connect'
const clientId = 'https://cafes.example/oauth-client-metadata.json'
// Serve this JSON at clientId (a static file or a route).
const metadata = clientMetadata({ clientId, clientName: 'Cafe finder' })
// On every page load: finishes a sign-in when the page is the redirect back, else restores a kept session.
const session = await restore({ clientId })
if (!session) await signIn({ clientId, handle: 'alice' }) // alice.techne.town, once techne.town is live (see below)
else {
const techne = connect(session, TECHNE_PLATFORM_DID)
}
- Who: with no
handle, the sign-in starts at the default PDS (techne.town), which asks who you are. A bare name (alice) is a handle on the default handle domain. A full handle, a DID or a PDS URL from any PDS is used as given.signUp({ clientId })opens the PDS's create-account screen. - Scopes:
scopes()is what it asks for by default:atproto, the platform'sauthReadandauthWritepermission sets with the platform as their audience, and arepo:scope for the publiccommunity.lexicon.*records.clientMetadatalists the same scopes, so the two always match. Passscopeto ask for other scopes, and pass it to both. - Development: on
localhostor127.0.0.1you can leaveclientIdout. The SDK then uses atproto's loopback client, so you don't need to host anything. - Sessions: the library refreshes tokens itself.
restorereturns the session,nullwhen nobody is signed in, and rejects when a sign-in or refresh failed.signOut({ clientId, session })revokes the session.onSessionDeletedtells you when the library drops a session. Astatepassed tosignIncomes back fromredirectState({ clientId })on the page the redirect lands on. - Another deployment:
platformnames the platform's DID (TECHNE_PLATFORM_DIDby default).pds,plcUrlandhandleDomainname the PDS people sign in at, its PLC directory and its handle domain. Pass them the same way to every call.
Before the Techne release: the techne.town defaults (TECHNE_PLATFORM_DID, the techne.town PDS and handle domain) go live at the Techne release. Until then, signIn() without pds fails: pass pds, plcUrl, handleDomain and platform explicitly, and ask the Techne team for the current staging values.
Layout
| Path | What |
|---|---|
src/ |
the TypeScript package. src/generated/, src/catalog/catalog.gen.ts and src/wasm/*.gen.* are generated and committed |
crates/techne-sdk/ |
the Rust crate, with the shared fixtures (fixtures/cases.json) both languages must answer identically |
inputs/ |
the codegen inputs, copied by scripts/inputs-sync.mjs and pinned in inputs.pin.json: inputs/lexicons/ (the lexicons repo at lexicons.tag) and inputs/platform-api/ (the platform's routed method lexicons, the atproto methods it serves and its NSID rename map, at platformApi.rev) |
lexicons-base/ |
standard atproto repo-op lexicons the generated client needs |
dev-pds/ |
a real atproto PDS and PLC in one Node process, for end-to-end runs against a platform |
scripts/ |
codegen (gen.sh), the WebAssembly build (build-wasm.sh), the package build (build.mjs), the gate |
Changing it
- A lexicon changed: tag the lexicons repo. Set
lexicons.tagandlexicons.commitininputs.pin.jsonand thetechne-lexicon-checktag inCargo.toml, then runcargo update -p techne-lexicon-checkandnode scripts/inputs-sync.mjs --lexicons <lexicons checkout> --platform <platform checkout>. Then runnpm run codegenand commit. - The platform's API changed: set
platformApi.rev(a platform tag for releases), then run the same sync and codegen. - The Rust logic changed: edit
crates/techne-sdkand runnpm run codegen(it rebuilds the WebAssembly). The fixtures must still agree.
npm run codegen needs the pinned Rust toolchain (rust-toolchain.toml) with the wasm32-unknown-unknown target, and npx for binaryen. The build is reproducible, so the gate can diff it against the committed one.
The gate
package builds dist/, then loads every subpath through exports in plain Node and runs the fixtures through the built WebAssembly and wasm2js modules. It also checks the npm pack file list with a dry run. rust runs the same fixtures through the crate.
Releasing
See RELEASING.md.