Authentication
Your backend mints short-lived JWTs; Pointy verifies them. You hold the private key, Pointy holds only the public half, so it never stores your users or your signing key. Register keys on the developer dashboard.
Install the CLI
The fastest way to configure all of this is the pointy CLI — it
logs you in via your browser and registers your keys from the terminal:
curl -fsSL https://sync.pointy.ink/static/cli/install.sh | sh
pointy login
pointy quickstart # prints wsUrl + iss + aud + a JWT claims template
pointy key set --jwks https://your-backend.example/.well-known/jwks.json
The CLI checks for new versions once a day and updates itself with
pointy update.
Register keys
Pointy verifies a token's signature against the key registered for its
iss (your account id) and kid. Choose at least one method:
- JWKS endpoint (preferred) — host your public keys as a JSON document; Pointy caches it and re-fetches on rotation, so you keep full control and rotate yourself.
- Static key (fallback) — paste a public key (Ed25519 / RS256 / ES256) into the dashboard. Uploaded keys win on a
kidcollision.
Host a JWKS endpoint
Serve a document like this at a URL Pointy can reach:
// .well-known/jwks.json
{
"keys": [{
"kty": "OKP", "crv": "Ed25519", "kid": "key-1",
"x": "<base64url of the 32-byte public key>"
}]
}
Next.js route:
// app/.well-known/jwks.json/route.ts
export async function GET() {
return Response.json({ keys: [{
kty: "OKP", crv: "Ed25519", kid: "key-1",
x: process.env.PUBLIC_KEY_B64,
}] })
}
Go:
func jwksHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"keys": []any{map[string]any{
"kty": "OKP", "crv": "Ed25519", "kid": "key-1",
"x": pubB64,
}},
})
}
Mint a JWT
Sign a short-lived token with your private key. The header kid must
match a key you've registered; the claims scope it to one doc and one permission
level:
{
"iss": "acct_clientco", // your account id
"sub": "user_42", // unique id for one of your users
"doc": "<doc id>", // the doc_id you created
"perm": "write", // "read" or "write"
"aud": "https://sync.pointy.ink",
"exp": 1750000900 // unix expiry — keep it short-lived
}
Go (golang-jwt/v5):
claims := struct {
Doc string `json:"doc"`
Perm string `json:"perm"`
jwt.RegisteredClaims
}{ Doc: doc, Perm: "write", RegisteredClaims: jwt.RegisteredClaims{
Issuer: "acct_clientco", Subject: sub,
Audience: jwt.ClaimStrings{"https://sync.pointy.ink"},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15*time.Minute)),
}}
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
tok.Header["kid"] = "key-1"
return tok.SignedString(privKey)
Node (jose):
const token = await new SignJWT({ doc, perm: "write" })
.setProtectedHeader({ alg: "EdDSA", kid: "key-1" })
.setIssuer("acct_clientco")
.setSubject(sub)
.setAudience("https://sync.pointy.ink")
.setExpirationTime("15m")
.sign(privateKey)