Complete API documentation for Stratofusion's backend endpoints.
The combined Google, OneDrive, and Dropbox listing/OAuth endpoints can return
HTTP 200 with { "url": "..." } when provider credentials are missing.
For GET folder reads (folderId supplied), the shared apiGet client translates
that response into AUTHENTICATION_REQUIRED, retaining the requested service
and account identity. The drive view then requests reconnection instead of
interpreting the response as an empty listing. Explicit add-account and OAuth
callback requests retain their URL response; a valid { "items": [] } listing
still represents an empty folder. Saved connection metadata alone does not
establish that provider credentials are available.
OAuth account-type mismatches retain the selected and detected account types
in dialogs and toast feedback, with a Choose Account Type action. The
generic authentication formatter must not replace this explanation with a
disconnect/reconnect instruction. Dropbox's account_type API field determines
the detected type; a Google sign-in method or personal email does not establish
that the Dropbox account is classified as personal. Personal and Work/School
Dropbox connections currently request the same user-level OAuth scopes.
Running backup/sync job responses expose optional
operationSummary.current.progressView with label, percentage (nullable),
indeterminate, listedCount, checkedCount, and countersText.
The server derives this provider-neutral view from the worker snapshot.
Unknown scan totals produce a null percentage; counts describe observed
listing/checking work and can cover both endpoints. Existing summary fields
remain available for older clients.
Production:https://stratofusion.io/api on the authoritative OVHcloud VM.
Legacy: a retained Vercel production deployment exists for approved,
database-reconciled recovery only; it is not the normal API base URL.
API Conventions
All endpoints use REST principles.
All requests/responses use JSON format.
All endpoints require authentication unless explicitly documented as public, such as webhooks, OAuth status/callback helpers, policy docs, signed event streams, or the allowlisted article-engagement endpoint.
All responses follow the standardized ApiResponse format.
Debug and dev endpoints require server-side dev-or-higher role checks; frontend-only checks are not sufficient.
Download-session and delete endpoints reject unauthenticated callers before provider-token lookup or destructive work.
Browser requests using POST, PUT, PATCH, or DELETE must carry exact
same-origin Origin or Fetch Metadata evidence. Rejected request-integrity
checks return 403 with Cache-Control: no-store before authentication or
handler execution.
Behind Caddy, exact-origin comparison uses the public request Host
(including its port) and the request URL scheme, rather than the standalone
Next.js listening address. Sibling hosts, different schemes/ports, and
cross-site requests remain rejected; no sync-specific exemption is used.
Credentialless webhook, cron, and internal-service calls remain supported and
must pass the endpoint's signature, bearer, or service authentication.
Public article engagement
POST /api/analytics/articles
Content-Type:text/plain;charset=UTF-8
This best-effort public endpoint records aggregate editorial engagement. It
accepts a JSON body up to 2 KiB and returns 204 for a supported event. The
allowlist covers article views, audience-filter selection, library/homepage/
related-article clicks, CTA clicks, and article-attributed signup start or
completion.
Only fixed event/source/destination values, a supported audience, and safe
article slugs are retained. Unknown fields are discarded, invalid combinations
return 400, oversized payloads return 413, and browser requests explicitly
marked cross-site return 403. The handler intentionally does not add IP
addresses, user agents, referrers, query strings, account identifiers, or email
addresses to its structured ARTICLE_ENGAGEMENT application logs.
Authentication
OAuth Flow
1. Initiate OAuth
GET /api/{service}?accountId={accountId}&popup=true
Parameters:
service - Service type (google, onedrive, dropbox).
accountId - Account identifier (optional, defaults to default).
Response:
Returns the provider authorization URL in the standard response envelope.
Returns 403 when the request host is not a registered stable OAuth host or
the exact configured Phase-B rehearsal origin.
The direct request authority and X-Forwarded-Host, when both are present,
must agree after default-port normalization. A forwarded header cannot
authorize an otherwise unsupported request host.
2. OAuth Callback
GET /api/{service}?code={code}&state={state}
Parameters:
code - Authorization code from service.
state - Opaque StratoFusion flow context returned unchanged by the provider.
Response:
Redirects to the validated public application origin with success/error.
Returns 400 before token exchange when state is missing, malformed,
expired, legacy unsigned, or fails its HMAC signature. State is valid for ten
minutes and includes a random nonce; callers must treat it as opaque.
Initiation returns 401 if no authenticated Clerk user is available to bind
the provider flow.
Callback redirects use the validated NEXT_PUBLIC_SITE_URL; raw forwarded
host, port, and protocol values are never emitted as redirect targets.
Google Drive, OneDrive, and Dropbox are the implemented direct OAuth service
values. Scaffolded future providers are not accepted here.
GET and POST /api/auth/clerk-oauth enforce the same incoming-host policy
before reading Clerk accounts or attempting provider auto-connection.
3. Check Auth Status
GET /api/auth/status?service={service}&accountId={accountId}
POST /api/auth/disconnect
Content-Type:application/json{"service":"google","accountId":"default"}
5. Logout Token Cleanup
POST /api/auth/logout?service={optional-supported-service}
Requires an authenticated Clerk session. Deletes encrypted provider-token rows
for the requested supported service, or all provider-token rows when omitted,
while preserving connected-account metadata and user preferences. Unsupported
service values return 400; explicit disconnect owns account removal.
Response Format
Stratofusion is transitioning to a standardized response format managed by the src/shared/api module. This aligns with the Ports & Adapters architecture by ensuring consistent communication across different providers.
Standard Success Response
Newer endpoints return the data directly as a JSON object, while legacy endpoints may still use the success: true wrapper.
disposition is the operation the provider actually completed: trash or
permanent. It is not copied from useTrash. For example, if permanent delete
is unavailable and the provider successfully falls back to its recycle bin,
the result is disposition: "trash" and trashedOnly: true.
POST /api/delete/stream accepts the same request and returns NDJSON progress.
Each item_complete.result and the final response use the same actual-outcome
contract.
POST /api/search/basic
Content-Type:application/json{"query":"resume","services":["google","onedrive"],"accountIds":{"google":"default","onedrive":"default"}}
Note: The job management system supports both backup jobs and sync jobs through a unified service layer architecture. All endpoints automatically detect and handle both job types.
Architecture Overview
API Routes (HTTP Layer)
↓
Job Service (Business Logic)
↓
Job Mappers (Data Transformation)
↓
Database Functions
Key Features:
Automatic job type detection.
Unified error handling.
Type-safe interfaces.
Consistent response formats.
Create Backup Job
POST /api/jobs/backup
Content-Type:application/json{"sourceService":"google","sourceAccountId":"default","sourceItems":[{"id":"file123","name":"document.pdf","isFolder":false}],"destinationService":"onedrive","destinationAccountId":"default","destinationFolderId":"folder456","schedule":"daily","scheduledTime":"14:30","timezone":"America/New_York"}
POST /api/jobs/sync
Content-Type:application/json{"mode":"two-way","sources":[{"service":"google","accountId":"default","folderId":"folder123","folderName":"Documents"}],"destination":{"service":"dropbox","accountId":"default","folderId":"root","folderName":"Dropbox"},"schedule":"daily","scheduledTime":"08:00","timezone":"Australia/Sydney","maintainStructure":true,"applyFilters":true}
maintainStructure - Whether to preserve folder structure (default: true).
applyFilters - Whether to apply transfer filters (default: true).
Behavior:
The endpoint creates the job record and returns immediately. It does not recursively enumerate folders during job creation.
Quota and file-size validation happens during the launch phase before the rclone operation starts.
Large one-way syncs use a 30-minute meaningful-progress stall window instead of a fixed 12-hour runtime cap. The window also covers silent startup; repeating speed/current-file text alone does not reset it. Two-way sync runtime budgets are unchanged.
GET /api/storage/quota?service={service}&accountId={accountId}
Supported services are google, onedrive, and dropbox. Dropbox quota reads first verify that the token-bound Dropbox email matches the saved account and that the account is still usable for its stored account type. If credentials belong to another Dropbox identity, or a stored Dropbox Business account is disabled, cancelled, downgraded, or otherwise no longer accessible, the endpoint returns a normalized accountHealth payload instead of quota data or generic failure text. Successful Dropbox responses normalize the provider's tagged individual/team allocation union so member-specific usage is never combined with team-wide capacity (or vice versa).
{"success":false,"error":"Dropbox Business access is no longer available for this account. Reconnect a Dropbox Business account or remove the old account.","errorCode":"ACCESS_DENIED","accountHealth":{"service":"dropbox","status":"plan-changed","reason":"business-access-lost","needsReauth":false,"expectedAccountType":"business","providerAccountType":"personal"}}
A token-bound identity mismatch returns 401 with
accountHealth.reason = "account-mismatch", needsReauth = true, and the
normalized saved/provider emails so the user can reconnect the intended
Dropbox account.
GET /api/search/enhanced?q=project%20plan&mode=ai&services=google&accountIds=google-account-1
POST /api/search/enhanced is also supported for clients that prefer a JSON
body. It normalizes into the same handler as GET.
POST /api/search/enhanced
Content-Type:application/json{"q":"project plan","mode":"ai","maxResults":25,"services":["google"],"accountIds":["google-account-1"],"folderId":"folder-1"}
Supported modes are basic, fulltext, and ai. AI mode requires the user's
subscription entitlement plus runtime semantic-search configuration. If the AI
runtime kill switch or semantic infrastructure is disabled, the endpoint returns
503 with AI search is disabled by runtime policy before consuming search
quota.
AI indexing endpoints require an authenticated user and only operate on jobs
owned by that user. Current indexing eligibility is limited to the explicit
AI-indexing allow-list: deterministic text-like formats, PDFs with extractable
embedded text, DOCX files with extractable text, XLSX spreadsheets with
extractable visible cell values, PPTX presentations with extractable slide
text and speaker notes, standalone PNG/JPEG images when OCR is enabled, plus
Google Workspace files exported from Google Drive before extraction. Supported
Google Workspace MIME types are application/vnd.google-apps.document,
application/vnd.google-apps.spreadsheet,
application/vnd.google-apps.presentation, and
application/vnd.google-apps.drawing. Google Docs export to DOCX, Google
Sheets export to XLSX, Google Slides export to PPTX, and Google Drawings export
to PDF. Successful Workspace indexing records
extraction_method: "google_workspace_export" while preserving the original
Workspace MIME type in indexed metadata. OCR is disabled by default and
requires AI_SEARCH_OCR_ENABLED=true; without it, PNG/JPEG files are skipped
with an OCR-required reason. PDF indexing does not render pages for OCR in
Phase 6A. Before DOCX, XLSX, or PPTX parser dispatch, OpenXML ZIP metadata is
checked for bounded entry count, entry and aggregate expanded bytes,
compression ratio, encrypted entries, unsupported ZIP64 size markers, and
unsafe entry paths. Rejected packages produce sanitized extraction failures.
DOCX indexing uses deterministic plain-text extraction for .docx
extension and OpenXML document MIME inputs, including parameterized MIME values;
it does not support legacy .doc, password-protected or encrypted documents,
embedded image extraction, OCR over embedded images, or complex formatting
preservation. XLSX indexing reads visible worksheets
row by row with --- Sheet: SheetName --- delimiters and cell values rather
than formula syntax; it does not support legacy .xls, hidden sheets, charts,
images, macros, or OCR. Empty XLSX files skip with extraction_xlsx_no_text;
corrupt, malformed, timed-out, or password-protected XLSX files use sanitized
extraction_xlsx_parse_failed or extraction_xlsx_encrypted taxonomy codes.
PPTX indexing reads non-hidden slides in presentation
order, emits Slide N: delimiters, and includes speaker notes when the slide
relationship exposes an identifiable notes body placeholder. Empty PPTX files
skip with extraction_pptx_no_text, encrypted PPTX files skip with
extraction_pptx_encrypted, and corrupt, malformed, or timed-out PPTX parsing
fails with extraction_pptx_parse_failed; PPTX failure messages are sanitized.
It does not support legacy .ppt, ODP, Keynote, embedded media, animations,
transitions, timings, OCR, master slide/layout template text, or image alt text.
Empty, corrupt, encrypted, image-only/scanned, or oversized PDFs, DOCX files,
XLSX files, PPTX files, and exported Google Workspace content can be skipped or
failed with an item-level reason instead of producing semantic vectors.
Exported Workspace content is not cached; each indexing run exports
fresh bytes and preserves original file metadata in indexed records. Google
Forms, Sites, Maps, Fusion Tables, Jamboard, Apps Script, Shortcuts, third-party Drive app
files, non-PPTX presentations, other Office formats, GIF, TIFF, BMP, WebP,
HEIC, media, archives, cloud OCR providers, OCR caching, and unknown binary
formats remain unsupported for AI indexing. Export timeout, permission, quota,
network/provider, unsupported-type, invalid-provider, and operational OCR
provider failures fail the affected indexing item with sanitized reasons.
Indexing execution performs a semantic-index backend readiness check before
provider discovery; if the configured vector backend cannot bootstrap its
collection, the job is finalized as failed with a sanitized semantic-index
failure reason and no item rows are created.
Download stream proxy
GET /api/download/stream?service={google|dropbox}&token={signed-token} accepts
only a bounded signed-token shape. The configured worker target must be the
fixed local development endpoint or a public pathless HTTPS origin. Upstream
redirects are refused, and connection failures return a generic 502 response
without the upstream URL or raw network error.
List Recent AI Indexing Jobs
GET /api/search/indexing/jobs/recent?limit=20
Returns active and recent AI indexing jobs for the current user. Active jobs are
sorted first so clients can use the response as the REST source of truth before
opening a live stream.
Stream AI Indexing Job Updates
GET /api/search/indexing/jobs/events
Server-Sent Events stream for /user/ai-indexing. The stream emits update
events with the same job-list shape as the recent-jobs REST endpoint, error
events when snapshot loading fails, heartbeat comments, and a reconnect event
before the server rotates the connection.
Get AI Indexing Job Details
GET /api/search/indexing/jobs/{jobId}
Returns a stable job status payload, including item-level statuses and failure
reasons when they were recorded.
Get AI Indexing Job Diagnostics
GET /api/search/indexing/jobs/{jobId}/diagnostics
Returns a user-owned job snapshot, progress, ETA when throughput is meaningful,
extraction method counts, taxonomy error groups, retryable/non-retryable counts,
extraction latency percentiles, slow-item samples, and capped failed/skipped
samples. Messages are sanitized and do not include raw provider paths, extracted
text, stack traces, tokens, or raw provider errors.
Get AI Indexing Health
GET /api/search/indexing/health?service=google&accountId=account-1&scopeType=account
GET /api/search/indexing/health?service=google&accountId=account-1&scopeType=folder&resourceId=folder-1
Returns account or folder health metrics for a connected indexing account:
indexed, failed, unsupported/skipped, pending, stale/deleted counts, coverage
percent, success rates by extraction method, OCR confidence summary, recent jobs,
and whether reindexing is recommended because dirty scope state exists.
Get AI Indexing File Status
GET /api/search/indexing/files/status?service=google&accountId=account-1&resourceId=file-1
Returns the latest known indexed-file state and latest job-item state for the
resource identity, including extraction/index status, last indexed timestamp,
sanitized taxonomy code/message, retryability, recommendation, OCR metadata,
chunk count, text length, extraction duration, and reindex recommendation.
Cancel AI Indexing Job
POST /api/search/indexing/jobs/{jobId}/cancel
Allowed for pending, running, and retrying jobs. The route marks the job
cancelled and aborts the in-process runner when it is active, which prevents
stale recovery from restarting the job. Successful cancel controls emit an
ai-indexing-cancel activity/audit log event with the source job ID.
Retry AI Indexing Job
POST /api/search/indexing/jobs/{jobId}/retry
Allowed for failed, cancelled, and partially_completed jobs. The route
creates a new indexing job from the original account or folder scope and leaves
the original job as history. Successful retry controls emit an
ai-indexing-retry activity/audit log event with the source job ID and retry job
ID in metadata.
Dismiss AI Indexing Job
POST /api/search/indexing/jobs/{jobId}/dismiss
Allowed for terminal jobs. The route hides the job and job-item history from the
user-facing recent list with a dismissed_at marker; it does not delete job
rows, item rows, indexed-file metadata, or semantic vectors. Successful dismiss
controls emit an ai-indexing-dismiss activity/audit log event.
Dismiss Completed AI Indexing Jobs
POST /api/search/indexing/jobs/dismiss-completed
Soft-dismisses all visible completed AI indexing jobs for the authenticated
user. The batch action only updates each job row's dismissed_at marker, leaves
failed, cancelled, and partially completed jobs visible, and never deletes
job-item history, indexed-file metadata, or semantic vectors. Successful batch
dismiss controls emit one ai-indexing-dismiss activity/audit log event with
the dismissed job count in metadata.
Admin Operations
Admin endpoints require a signed-in user whose Clerk ID is configured as an
admin and whose signed Clerk session records a verified second factor. Missing,
malformed, or unregistered second-factor evidence returns 403 with Admin multi-factor authentication required. This resource-level check is independent
of the middleware redirect boundary.
Get Infrastructure Overview
GET /api/admin/infrastructure/overview
Returns a no-store, read-only operational snapshot shaped as a fleet, currently
containing the single configured authoritative runtime node. The response
includes the sample time, overall/node status, release SHA when available,
CPU, memory, root-filesystem, network, uptime, rclone queue/capacity metrics,
observable service health, allowlisted Compose service replica/health status,
the exactly-one production scheduler invariant, backup freshness, sanitized
warnings, and allowlisted management links. Operational fields live under
nodes[].operations: collector, composeServices, scheduler, and
backup. Backup data contains timestamps and status summaries only.
Each links[] entry contains id, serviceId, label, href, kind, and
access. serviceId associates the action with an allowlisted service row;
kind describes the destination category and access is one of
same-origin, public, or private. Same-origin actions use fixed application
routes. Public links are credential-free HTTPS Grafana or GlitchTip origins in
the configured stratofusion.io domain family. Private database-tool links
accept only loopback HTTP URLs intended for SSH forwarding or HTTPS *.ts.net
origins. URLs containing credentials, query strings, fragments, public IPs, or
other domains are discarded. Clients must not construct or guess management
URLs.
Prometheus resource queries, application/database readiness, rclone health,
and operational telemetry are collected independently. A dependency failure
normally returns 200 with a partial snapshot, explicit unavailable values,
and warnings. Missing metrics and replica counts are null and must not be
interpreted as zero. Unauthenticated callers receive 401; signed-in
non-admin callers and admins without verified MFA receive 403. Unexpected
overview failures return a sanitized 503 without internal URLs, queries,
upstream bodies, hostnames, IP addresses, or credentials.
This endpoint exposes only allowlisted aggregate service state. It does not
expose container IDs/names, image names, environment variables, mounts, logs,
raw Docker responses, raw backup paths, private Cockpit access, or
infrastructure mutation controls.
Preview AI Search Reset
GET /api/admin/users/{userId}/ai-search/reset
Returns the user-owned AI semantic search state that will be removed, including
AI indexing jobs, job items, indexed-file metadata, dirty scopes, reconciliation
cursors, active local executions, and estimated vector chunks.
Reset AI Search State
POST /api/admin/users/{userId}/ai-search/reset
Deletes all semantic vectors for the target user first, then clears the user's AI
indexing tables in Neon. If vector deletion fails or the vector backend is not
configured, the Neon cleanup is not run.
Delete User
DELETE /api/admin/users/{userId}
Local and dev hard delete cancels user-owned backup/sync jobs, resets AI search
state, performs external provider and Stripe test cleanup, deletes remaining
Neon user data, then deletes the Clerk user last. Production hard delete remains
blocked.
Changes an existing managed subscription through Stripe. Immediate upgrades
are projected into Clerk and Postgres through the billing synchronization
boundary; downgrades follow Stripe billing-period scheduling. Direct Clerk tier
writes and caller-supplied expiry timestamps are rejected.
Creates a separate Pro or Unlimited application entitlement without changing
Stripe billing. expiresAt is optional; null means no expiry. The route
requires an administrator session with verified MFA and preserves the user's
Stripe metadata.