Wisent App

Developer documentation

Build on the Wisent App platform.

Canonical contracts for iOS, Android, the web application, backend services, private media, data migrations, and signed messaging integrations.

Client surfaces

iOS

  • Native Swift application with local-first chat and character workflows.
  • App Store release metadata and TestFlight automation are release inputs, not runtime configuration.
  • Room and persona state is owned by the authenticated account and synchronized through the application API.

Android

  • Native Android client using the same authenticated application contracts.
  • The package is com.wisent.android and is distributed through Google Play.
  • Platform UI may differ; conversation, persona, document, and media ownership rules do not.

Service APIs

Application API

The web and mobile clients authenticate before calling the stateless application API. That service owns business logic and database access, while generation is delegated to a separately deployed authenticated inference service.

MethodPathContract
POST/api/chatRun an authenticated conversation turn.
GET · POST/api/conversationsList or create conversations.
GET/api/conversations/searchSearch the caller’s conversations.
GET · DELETE/api/conversations/{id}Read or remove one owned conversation.
GET · POST · DELETE/api/personasManage caller-owned personas.
POST · GET/api/documentsUpload or list caller-owned documents.
GET · POST/api/data-transfer/{export|import}Download or atomically import the caller’s canonical Wisent account-data JSON.
POST/webhooks/user-inactivityReceive the authenticated inactivity event.
GET/health · /api/healthReport that the application process is accepting connections, deliberately without probing a dependency.
GET/readyz · /api/readyzMeasure the database, the model router and the object store, and answer 200 or 503 naming each dependency's operation, result and duration.
GET/api/versionReport the immutable content-addressed release the host is running, and the instance role it resolved.

Inference and platform bridges

Inference, training, and App Store automation deploy independently from the application API. The inference service owns tokenization and generation; training consumes queued offline work; the App Store bridge verifies webhooks and emits normalized GitHub dispatches.

MethodPathContract
POST/v1/generateGenerate text as a blocking response or server-sent event stream.
GET/healthReport inference readiness as ready, warming, or unhealthy.
POST/webhooks/app-store-connectVerify an App Store Connect webhook and dispatch its stable identifiers.
GET/healthReport App Store webhook bridge process health.

Off-platform messaging

Authenticated users can link an existing character and conversation to WhatsApp, Telegram, or iMessage. Linking returns a platform deep link; callers must treat delivery status separately from link creation.

MethodPathContract
POST/api/messaging/whatsapp/connectLink a phone number and character to WhatsApp.
POST/api/messaging/telegram/connectIssue a single-use Telegram deep-link token.
POST/api/messaging/imessage/connectLink a phone number and character to iMessage.
GET/api/messaging/statusRead the caller’s per-platform link state.

Private image service

Generated image objects remain private under stado://wisent-images. The application returns only short-lived HMAC-signed proxy URLs; clients never receive object-store credentials.

MethodPathContract
POST/image-generate/Create an image-generation job.
GET/image-generate/images/{uuid}.{ext}Read an authorized image through the signed proxy.
GET/image-generate/healthReport image pipeline readiness.
GET/healthReport process health.

Local MLX backend

Apple Silicon deployments provide local model inference, JWT authentication, control-vector training, and multi-armed-bandit notification scheduling without silently falling back to a remote provider.

MethodPathContract
POST/api/chatRun local model inference.
GET · POST/api/control_vectorsList or train control vectors.
GET/api/job/{id}Read the durable state of a training job.
GET · POST/api/mab/notifications/{schedule|statistics|pending|history}Operate and inspect the notification bandit.

Character text and lifecycle

The web application's manual Create wizard keeps the complete name, description and appearance entered by the user. Character Settings → Edit Character also keeps the complete name and description. These fields are not silently shortened. The manual wizard still requires a nonblank name and a processed profile image before completion.

Names and descriptions are saved in the character record. Appearance is an input to image generation, not a saved description: the image step removes surrounding whitespace and adds the selected avatar style's prompt modifier. Generation, crop, moderation and private image upload remain separate operations whose results must succeed; a preview alone is not proof of a stored image.

Authenticated clients create with POST /api/characters, read with GET /api/characters/ID, edit with PUT /api/characters/ID, and delete with DELETE /api/characters/ID. JSON creation and update bodies carry the complete name and description. Read the detail operation after an update to observe the saved values, rather than treating a success response as persistence evidence.

Missing authentication is refused. Another account cannot edit or delete an owned character; an inaccessible record can be reported as not found rather than forbidden. Database failures remain failures. Deletion reports deleted or unassigned: a public character with other people's conversations can lose its author association without being erased. New private qualification characters must be deleted, not merely unassigned.

Quick Mode keeps the complete description and requires only nonblank input in the application. Select Generate, review the saved private character or room, then select Create to open it. The final button navigates to records already saved by generation. Authenticated clients use POST /api/autogenerate/createwith description and type: character, room or auto. Character success returns the character; room success returns the room and its generated characters.

Wisent Backend still owns the accepted character-description range through AUTOGENERATE_DESCRIPTION_MIN_CHARS and AUTOGENERATE_DESCRIPTION_MAX_CHARS. The application sends a complete trimmed character description. For rooms it saves the complete trimmed scene and sends complete participant segments without a word or participant-count cut. A character-service refusal keeps its actual status and response body; the wizard displays the service detail without clearing the input. Blank requests are refused. Any failed room participant fails the room request and reports its cause. Generated characters from that attempt are discarded only while owned, private and unused; cleanup refusals retain the character ID and actual cause.

Both room creation forms and Room Settings → Edit room keep complete names and descriptions and do not impose the former participant cap. Authenticated POST /api/rooms requires name, an explicit characterIds array and optional description and entryMessages array. An empty participant array creates an empty room through the API; manual screens require a selected character. Missing arrays, blank names, duplicate IDs and another account's private characters are refused. The room wizard requires a nonblank name, not a local minimum name length. Its qualification checks whitespace refusal, short-name progression and a short name saved unchanged through the shorter room form. The full room wizard also keeps the complete image prompt, without a length cap or remaining-character counter. It trims surrounding whitespace, adds the existing wide-angle interior instruction and sends the chosen room style separately to generation; image-service failures remain failures.

Manual and quick creation save the private room and all participants in one create_private_room transaction. Read GET /api/rooms/ID and GET /api/rooms/ID/participantsto observe saved state, edit with PUT /api/rooms/ID, and delete with DELETE /api/rooms/ID. Deletion removes participants and retains existing conversations with their room association cleared.

The supported account-data CLI imports and exports canonical account records; it does not provide separate character-management commands. The web and authenticated server operations above are the character lifecycle interfaces.

From the application checkout, run node tests/characters/retention.mjs --fixture .build/character-fixture.jsonon a Stado-selected dedicated Weles host. The private fixture selects dedicated accounts, an exclusive authenticated English-language browser profile, real application and database URLs, meaningful long text, the model and avatar style, and independently retained evidence for the exact deployed source revision.

The journey creates, reads, edits, reloads and deletes a real private character; exercises actual image generation, moderation and display; checks authentication and ownership refusals; and reads the database after changes and cleanup. It keeps source revision, commands, exit statuses, complete responses, screenshots and supported Weles recordings under the checkout's ignored build directory. Missing prerequisites are blocked, not passed. Failed cleanup remains a failure, and media and analytics service records are not claimed to have been erased with the character.

Quick creation has a separate real journey: node tests/characters/quick-retention.mjs --fixture .build/quick-character-fixture.json. It requires an exclusive initially empty account, the real backend's declared bounds, independent application/backend/schema release evidence, and long character and room descriptions. It compares real backend refusals with application and wizard results, checks retained participant facts and actual saved rows, and checks partial room failure cleanup. Both manual room forms select beyond the former cap; the full wizard generates and displays a real room image. The editor saves and reloads long text, adds another participant and deletes through its confirmation dialog. Cleanup deletes rooms before characters through normal APIs and confirms the account's character and room state is empty. Existing data prevents the run; missing prerequisites are blocked, not passed. Retained conversations, media and analytics remain service records. These commands describe qualification, not a claim that an unrun journey passed.

Complete field semantics, request example, private fixture and evidence interpretation

API deployment

Explicit deployment inputs

  • The Publish API Stado release workflow publishes an immutable content-addressed archive, then activates its exact URI through Stado. Deployment coordinates belong to repository variables, not source code.
  • Set STADO_CONFIG_PATH and STADO_BIN_PATH to readable configuration and executable absolute paths on the activation runner. Set WISENT_API_TARGET_HOST to the registered destination host and WISENT_API_SERVICE_ENV_FILE to the absolute service environment file on that host.
  • In GitHub, configure these under Settings → Secrets and variables → Actions → Variables. The Actions → Publish API Stado release form and gh workflow run deploy-api.yml use those same declarations; relevant main-branch changes also use them.
  • To reconcile a release runner, run deploy/hosts/reconcile-release-runner from the backend checkout with WISENT_RUNNER_HOST, WISENT_RUNNER_NAME and an absolute WISENT_RUNNER_ROOT. Missing inputs and relative roots are refused before registration. STADO_BIN and GH_BIN select the installed clients; WISENT_RUNNER_OWNER_SERVICE and WISENT_RUNNER_SERVICE select the managed service identities.
  • The reconciler uses stado service ensure to persist the runner name and root with /bin/bash and the launcher argument. The launcher inherits its service account HOME. Registration credentials stay in a private, invocation-specific .build directory until local cleanup. GitHub Settings → Actions → Runners shows the named runner; the command succeeds only after GitHub reports it online, not merely after the unit starts.
  • The API launcher and release publisher read granted item fields through stado credentials get ITEM --field FIELD. Credential errors name the requested item or field; missing, empty or still-encrypted values are refusals, not usable credentials.

Activation result and refusals

  • Missing variables, an unreadable configuration, an unusable Stado binary or a relative service environment path stop activation before the service is changed. Stado reports target lookup, environment read and mutation failures from the selected host.
  • The workflow keeps the previous WISENT_BACKEND_API_RELEASE_URI before changing it. Activation requires the declared API unit to serve port 8000; a failure restores the previous URI and restarts that managed unit through Stado.
  • An archive URI or successful restart is not delivery evidence. Read the activation result and /api/version on the selected deployment; its immutable release URI must match the published URI. /readyz separately measures downstream dependencies.

Local MLX deployment

The local backend reads deployment-owned configuration, not a developer’s paths or account. Standalone MLX server variants and model diagnostics require MODEL_PATH, naming the actual local model directory or model repository. Missing or blank input stops startup with a message naming MODEL_PATH; a failed load retains its cause. This requirement does not replace the main authenticated API’s model-selection contract.

Configure APNS_KEY_ID, APNS_TEAM_ID, APNS_KEY_PATH and FASTAPI_ENDPOINT for the selected deployment. An empty example field is not a usable signing identity or callback. Deliver credentials through Stado’s secret synchronization, never in command arguments or committed environment files.

Cloud consumers require deployment-owned AWS_REGION, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, with AWS_S3_BUCKET_NAME for storage and LOG_GROUP_NAME for CloudWatch logging. No account key, bucket or log group is selected by source. Missing or blank inputs are refused as “CloudWatch logging requires explicit settings:” or “CloudWatch metrics requires explicit settings:”, followed by the missing names, never credential values. AWS_ENABLED selects S3 versus local storage; it does not disable the existing CloudWatch consumers. The unused ALERT_EMAILS setting and obsolete AWS_SECRET_KEY alias are removed; no alert-mail consumer used that setting.

From the local backend checkout, run bash infrastructure/runtime/launch ROLE with absolute WISENT_MLX_ROOT and WISENT_MLX_PYTHON inputs. The roles are api, gunicorn, worker and beat; trailing arguments go to the selected native process. The api role starts Uvicorn from app/: pass the deployment’s declared bind host, port and worker count through --host, --port and --workers. The launcher does not install or restart a managed service. Missing or relative paths and an unknown role return 2; an unavailable application directory or Python executable returns 1 and names the path. --help needs no inputs.

Declare the installed launcher through stado service ensure, with the selected host, /bin/bash as --from, the launcher and role as --arg values, and non-secret deployment paths as --env values. Stado owns the service identity, permissions, lifecycle and graphical service controls. Use one product unit per selected host, not source-owned systemd files or direct instance-update scripts. Read back the managed service and complete an actual API request or queue job before calling the deployment usable; process health alone does not establish that result.

Redis has Terraform and CloudFormation definitions under infrastructure/redis/. Choose one owner per resource. Terraform requires region, vpc_id, subnet_ids and deployment_name; node_count selects one through six nodes. Run terraform init, terraform apply -var-file=.build/deployment.tfvars.json, terraform output -json and terraform destroy with that same variables file and retained private state. Run these commands from infrastructure/redis/ and keep provider cache and state under its ignored .build/ directory. A single node disables failover and multi-AZ.

CloudFormation requires VpcId, SubnetIds and AllowedCidr and retains two-node HA. Use aws cloudformation deploy with the template and explicit parameters to create or update a named stack, describe-stacks to read it, and delete-stack followed by wait stack-delete-complete to remove it. Physical resource names belong to that stack. Both definitions retain plaintext Redis transport within the selected network. Check actual network membership and a write/read/delete from an authorized consumer, not just the returned endpoint. Keep connection output private.

The repository’s tests/models/local.test.mjs exercises real local inference and undeclared-model refusal. tests/infrastructure/redis.test.mjs provisions, updates, reads and deletes isolated real Redis resources through both definitions. Run them with node --test and the explicit private fixtures documented in the repository. Model qualification requires a Stado-selected Apple Silicon host and independently known model output; Redis qualification requires an authorized dedicated account and incurs cloud charges. Reports retain source revision, commands, status and observed state under .build/. Source delivery or a health response is not a qualification result, and these journeys do not qualify simulated prototype endpoints.

Run node --test tests/cloudwatch/runtime.test.mjs with the owner-only WISENT_MLX_CLOUDWATCH_FIXTURE described in the repository README. It requires an explicitly authorized dedicated cloud account, real chat identity and model, selected host, log group and independently known response, logs and metric values. The journey checks cloud account ownership, actual missing-setting refusal, real API inference through the normal launcher and provider log and metric readback. It stops only its own isolated process. An unconfirmed provider read is awaiting_observation; point WISENT_MLX_CLOUDWATCH_OBSERVATION at the retained report to read without another inference request. Source delivery, logger construction and process health do not qualify these consumers.

Complete runtime commands, private fixture fields and Redis lifecycle examples

Data and migrations

Canonical schema

  • wisent-supabase-wisent-app migrations are the schema source for the configured application database.
  • Pull-request CI checks migrations; the default branch is the deployment boundary.
  • Application code must not treat dashboard edits or local snapshots as canonical schema.

Optimization service

  • Supported steering comparisons include CAA, DAC, BiPO, REFT, and RepEng.
  • Requests identify steering type and layer configuration explicitly.
  • Optimization results are versioned artifacts; clients do not infer compatibility from filenames.

Bring existing account data

Wisent accepts its own canonical JSON export, identified by wisent.app.account-data.v1. The raw document is sent to authenticated POST /api/data-transfer/import; it is not wrapped in another object. Product-specific Byk clients usePOST /api/data-transfer/import?scope=byk, which rejects the whole file if any character, conversation, or project record is present. The 10 MiB request is completely validated before one atomic database call, so invalid input leaves no partial rows.

Where to import

  • Web: choose Import in the first onboarding screen or Settings → Support → Account data.
  • Android: use the account-import onboarding step or the account-data card in Settings.
  • iOS: use the first onboarding step or the account-data card in Settings; the legacy iOS Settings screen exposes the same action.
  • Each client uploads the selected JSON unchanged to the same authenticated operation and displays every imported, unchanged, conflicting, or rejected record result.
  • Byk clients use the server-owned scope=byk boundary, so they never parse or filter the selected file locally and cannot mutate non-Byk collections.

Accepted records

  • Every record has sourceOwnerId and sourceId. Characters accept name, description, greetings, suggestedFirstMessages, language, country, gender, isNsfw, visualPrompt, createdAt, and updatedAt; imported characters are private.
  • Conversations accept title, an optional imported-character source reference, characterName, modelName, systemPrompt, timestamps, metadata, and messages with sourceId, role, content, tokenCount, createdAt, metadata, and usesConversationCharacter.
  • Coding projects accept repository, issueNumber, instruction, COMPLETED or FAILED status, messages, proposedEdits, test and pull-request history, errors, and timestamps. Import never executes a project or its tests.
  • Byk data accepts paper-only portfolios, ad-hoc watchlist entries, and CLOSED or CANCELLED position history. Portfolio source references are portable; signal, strategy, published-signal, order, execution, and broker-credential references are not.

Duplicates and retained state

  • A repeated sourceOwnerId, sourceId, and record kind with the same content is unchanged.
  • If that source later has different content, the destination is reported conflicting and the existing row is retained.
  • An existing watchlist symbol and side is a conflict. Existing account data is not overwritten, and a destination default portfolio is not displaced.
  • Imported data remains in the normal account tables and is available after onboarding. Skipping import leaves an empty, usable account.

Explicit refusals

  • Unknown fields, malformed identifiers or timestamps, missing character or portfolio references, duplicate source identities, oversized collections, and more than 50,000 messages return HTTP 422 with the normal summary and records report; every count except rejected is zero.
  • Room or test conversations, live or auto-live portfolios, running projects, active positions, protective-order state, published signals, strategy references, orders, executions, broker credentials, generation configuration, and media URLs are not imported.
  • Authentication failure is HTTP 401 and a database refusal is HTTP 503, each with { detail: string }. Both leave the destination unchanged; clients never display or log credential contents.

Exact document contract

The required top-level keys are format and exportedAt. Supported optional arrays arecharacters, conversations, projects, portfolios, watchlist, andpositions; omitted arrays are empty and unknown keys are rejected. The canonical full export emits every array. Collection limits are 500 characters, 1,000 conversations, 1,000 projects, 100 portfolios, 1,000 watchlist entries, and 10,000 positions.

Portfolio fields are exactly sourceOwnerId, sourceId, name, accountSize, riskPct, heatCapPct, mode, currency, isDefault, riskTier, volatilityRiskPct, dailyLossLimitPct, weeklyLossLimitPct, maxPositions, grossExposureCapPct, netExposureCapPct, maxLongExposurePct, maxShortExposurePct, maxSectorExposurePct, maxAverageCorrelation, maxDrawdownPct, dailyPnl, weeklyPnl, buyingPower, cashBalance, maxPositionAdvPct, maxSingleNamePct, maxGroupExposurePct, globalKillSwitch, reconciliationBlocked, reduceOnly, allowAutoLive, equityPeak, scaleOutPlan, trailingStopR, timeStopDays, policyVersion, createdAt, updatedAt.mode must be paper, allowAutoLive must be false, and live risk tiers are refused.

Watchlist fields are exactly sourceOwnerId, sourceId, symbol, side, notes, createdAt, updatedAt. Position fields are exactly sourceOwnerId, sourceId, portfolioSourceOwnerId, portfolioSourceId, symbol, side, status, entryPrice, stop, target, shares, notional, riskDollars, multiplier, underlyingGroup, openedAt, closedAt, realizedR, normalizedR, fees, notes, initialStop, finalStop, averageEntry, filledQuantity, remainingQuantity, grossPnl, netPnl, feesPaid, commissionsPaid, plannedR, realizedGrossR, realizedNetR, entrySlippageR, exitSlippageR, costsR, managementDeltaR, signalAlphaR, executionAlphaR, sizingContribution, maeR, mfeR, exitEfficiency, exitReason, protectionStatus, protectedQuantity, protectiveOrderIds, protectiveOcaGroup, protectionVersion, timeStopAt, favorablePrice, adversePrice, managementState, managementVersion, version, createdAt, updatedAt.status must be CLOSED or CANCELLED; protectiveOrderIds must be empty and no active protective quantity or OCA group is accepted.

CLI

Install the wisent-app executable from the canonical source repository. It uses the same Supabase bearer session and API operation as the graphical clients, and never accepts a token as an argument. Point --session-file at a mode-0600 standard Supabase session JSON containing access_token, or pipe an access token from an existing credential manager with --token-stdin.

npm install --global git+https://github.com/wisent-ai/wisent-app.git
wisent-app export ./wisent-account.json --session-file ~/.config/wisent/session.json
wisent-app import ./wisent-account.json --session-file ~/.config/wisent/session.json
security find-generic-password -s wisent-access-token -w | wisent-app import ./wisent-account.json --token-stdin

Export creates a new owner-only file and refuses to overwrite an existing path. Import prints the summary and one line per record without printing content or credentials; conflicts or rejections produce exit status 2. See the complete CLI reference.

Tour messaging bridge

The FastAPI bridge exposes GET /health and signed POST /webhook. A webhook is admitted only for x-webhook-event: message.received, a timestamp no more than five minutes old, and an HMAC-SHA256 signature over <timestamp>.<raw body>.

  1. Resolve the sender through Supabase imessage_users; unlinked senders receive account-link guidance.
  2. Detect explicit travel or location intent.
  3. Call POST /api/messaging/tour-chat with { messages, user_id, location? } and x-agent-secret.
  4. Reply through Ultrareach POST /v3/chats/{chat_id}/messages with the service bearer.

Required secrets: SERVICE_API_TOKEN, AGENT_TOOL_SECRET, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and TOUR_BRIDGE_SIGNING_SECRET(S). Optional bases default to local Ultrareach on port 8080 and https://app.wisent.ai.