Manual implementation
Build VibeID sign-in from scratch.
Use this guide when you are building outside the official packages, auditing the protocol, or writing an integration for another framework.
Use packages when you can
For Next.js apps, start with @vibe-id/next. This page exists for custom servers, framework adapters, and developers who need the wire-level details.
Routes to implement
Your app needs a request endpoint, a callback endpoint, a browser status endpoint, and normal session endpoints.
| Method | Route | Purpose |
|---|---|---|
| POST | /api/vibe-id/request | Create a short-lived challenge and return a deep link plus status URL. |
| POST | /api/vibe-id/callback | Receive the VibeID app result and verify the returned signature. |
| GET | /api/vibe-id/status/[requestId] | Let the browser poll until the request is approved, rejected, failed, or expired. |
| GET | /api/vibe-id/session | Read the current browser session. |
| POST | /api/vibe-id/logout | Delete the current browser session. |
Challenge payload
VibeID signs one exact string. Store that exact string server-side and verify the returned signature over the same bytes.
signin.v1.<requestId>.<nonce>.<issuedAt36>.<expiresAt36>.<originBase64Url>| Field | Rule |
|---|---|
| requestId | Unique server-generated id for this sign-in attempt. |
| nonce | Base64url string with at least 96 bits of random entropy. |
| issuedAt36 | Unix timestamp in milliseconds, encoded in base36. |
| expiresAt36 | Unix timestamp in milliseconds, encoded in base36 and greater than issuedAt36. |
| originBase64Url | Base64url UTF-8 origin, normalized to scheme + host + optional port. |
Deep link contract
Generate vibe-id://sign links. Short aliases keep QR codes compact; long names are accepted too.
const callbackUrl = new URL("/api/vibe-id/callback", siteUrl).toString();
const deepLinkUrl =
`vibe-id://sign?p=${encodeURIComponent(challenge)}` +
`&c=${encodeURIComponent(callbackUrl)}` +
`&k=signin`;| Name | Alias | Required | Meaning |
|---|---|---|---|
| payload | p | yes | The exact payload to sign. For sign-in, this is the signin.v1 challenge. |
| callback | c | yes | The HTTP(S) endpoint where VibeID posts the signed result. |
| kind | k | recommended | Use signin for browser sign-in UX. |
| requestId | r | optional | Explicit request id. If omitted, VibeID extracts it from signin.v1. |
| didHint | d | optional | Preferred local DID. The user can still choose another local identity. |
| v | - | optional | Protocol version. Defaults to 1. |
Callback payload
VibeID posts JSON to your callback endpoint. Treat successful callbacks as untrusted until the signature verifies.
{
"status": "ok",
"v": "1",
"kind": "signin",
"requestId": "abc123",
"signature": "<base64>",
"did": "did:vibe:p256:<base64url-compressed-public-key>",
"alg": "P-256",
"profile": {
"displayName": "Personal",
"initials": "P",
"theme": {
"key": "orbital",
"displayName": "Orbital",
"startColor": "#24335D",
"accentColor": "#3468F7",
"endColor": "#76D8F6",
"avatarColor": "#84D8F4",
"surfaceColor": "#EEF6FF",
"surfaceAccentColor": "#D7E6FF"
},
"avatarUrl": null
}
}| Field | Meaning |
|---|---|
| status | ok or error. |
| v | Protocol version. Current value is 1. |
| kind | signin for this flow. |
| requestId | The request id from the challenge. |
| signature | Base64 signature. Required when status is ok. |
| did | did:vibe:p256:<base64url-compressed-public-key>. Required when status is ok. |
| alg | P-256. |
| profile | Optional display metadata. Verify the signature before storing it. |
| error | Machine-readable error code when status is error. |
| message | Optional human-readable failure detail. |
{
"status": "error",
"v": "1",
"kind": "signin",
"requestId": "abc123",
"error": "user_rejected",
"message": "The request was rejected on device."
}Signature verification
The DID suffix is the base64url-encoded compressed 33-byte P-256 public key. Reconstruct the public key and verify the signature over the stored challenge.
- Require
alg: "P-256". - Decode
did:vibe:p256:...as a compressed P-256 key. - Verify SHA-256 ECDSA over the exact stored challenge string.
- Accept DER and IEEE P1363 ECDSA encodings during the pilot.
- Reject missing, expired, or already completed requests before creating a browser session.
Browser session handoff
The callback is delivered by the mobile app, not the waiting browser. Store the verified result on the server, then let the browser polling endpoint set the HttpOnly session cookie when it observes success.
Profile metadata
A successful callback can include display metadata from the selected VibeID persona. This is not identity proof. Store it only after the DID signature verifies.
type VibeProfile = {
displayName: string;
initials: string;
theme: {
key: "orbital" | "nebula" | "flare" | "tidal";
displayName: string;
startColor: string;
accentColor: string;
endColor: string;
avatarColor: string;
surfaceColor: string;
surfaceAccentColor: string;
};
avatarUrl: string | null;
};Trim and length-limit text, accept only known theme keys and #RRGGBB colors, and only keep avatar URLs when they are HTTPS URLs controlled by VibeID. Older app versions can omit profile metadata, so keep a DID-derived fallback.