Provider Layer

Sign inSign up

Marvia-22

The domain is permanent, the infrastructure is replaceable

Every external capability is an interface with a mock, an adapter and one line of registration. Swapping mock for PostgreSQL, or PostgreSQL for another PostgreSQL, changes no UI, no domain rule and no business logic.

Dependency direction

  1. Marvia UI
  2. Application services
  3. Domain
  4. Repository and provider interfaces
  5. Adapter layer
  6. Infrastructure (PostgreSQL, R2, Queues, KV)

Quick actions

Forbidden

  • supabase.from() in a component or route
  • prisma / drizzle / firebase clients in the UI or domain layer
  • process.env.X read outside the typed configuration layer
  • a raw provider error rendered to a user
  • service credentials, OAuth secrets or payment keys reachable from browser code

Provider interfaces

Nine capabilities, one shape each

A provider declares metadata, a health probe and its methods. Nothing else in the codebase knows which implementation is bound.

auth

The UI reads a session from the abstraction only; no sign-in SDK is ever imported by a component.

getSession()getCurrentUser()signIn()signOut()refreshSession()getAccessToken()onAuthStateChange()
Active
Google, GitHub, Mock
Planned
Email, Magic link, Passkey, SSO, Enterprise IdP

database

The domain never depends on a driver. Repositories sit above this interface, queries below it.

query()transaction()connect()disconnect()
Active
PostgreSQL, Mock
Planned
Supabase PostgreSQL, Neon, Self-hosted, Cloud SQL

storage

Buckets are named capabilities, not paths: extension-packages, screenshots, reports, exports, content-assets, marketplace-assets.

upload()download()delete()exists()getSignedUrl()
Active
Cloudflare R2, Mock
Planned
S3, Supabase Storage, R2-compatible

payment

Pricing, coupons and revenue share belong to monetization; only the transport is provider-specific.

createCheckout()getPayment()verifyPayment()refundPayment()cancelPayment()createCustomer()
Active
Telegram manual, Paddle, Duitku, Mock
Planned
Stripe, PayPal, Midtrans, Xendit

ai

AIOS composes context and prompts; the provider only executes and reports usage.

generateText()streamText()generateStructuredOutput()estimateUsage()getModelInfo()
Active
OpenAI, Gemini, Anthropic, OpenRouter, Mock
Planned
BYOK, Local models

queue

Job kinds are a closed set: snapshot.generate, report.generate, connector.sync, ai.job, marketplace.validate, notification.dispatch, billing.reconcile.

enqueue()schedule()retry()cancel()getStatus()
Active
Cloudflare Queues, PostgreSQL job queue, Mock
Planned
External scheduler

cache

Cache is an accelerator. A miss is always answerable from the repository.

get()set()delete()has()invalidate()
Active
Cloudflare KV, In-memory, Mock
Planned
Redis, Upstash

search

Search results are domain documents with a score, never rows from a vendor index.

search()index()remove()suggest()
Active
PostgreSQL full text, Mock
Planned
External search, Vector search

notification

No business rule hardcodes a channel; the service picks a category, the provider picks the wire.

sendInApp()sendEmail()sendTelegram()sendWebhook()sendPush()
Active
In-app, Webhook, Mock
Planned
Email transport, Telegram bot, Push

Domain, repositories and services

Business logic has exactly one home

Twenty-nine permanent domain objects, twenty-nine repository interfaces and eleven application services. A component composes none of them by hand.

Domain objects

WorkspaceUserOrganizationMembershipProjectSnapshotUniversalWebsiteObjectKnowledgeObjectReportFindingMissionSkillCompetencyExtensionConnectorCapabilityCommandLearningPathCreditWalletTransactionSubscriptionPaymentMarketplaceAssetPublisherBlueprintContentNotificationAuditEvent
  • A domain object never carries a provider-specific field, id format or client handle.
  • Business rules live in the domain and the services, never in a component.
  • Persistence flows through a repository interface; a service never opens a connection.
  • Renaming a provider must not touch a single domain type.

Repositories

UserRepositoryOrganizationRepositoryMembershipRepositoryWorkspaceRepositoryProjectRepositorySnapshotRepositoryReportRepositoryFindingRepositoryKnowledgeRepositoryKnowledgeGraphRepositoryMissionRepositorySkillRepositoryCompetencyRepositoryExtensionRepositoryConnectorRepositoryCapabilityRepositoryCommandRepositoryLearningRepositoryCreditRepositoryWalletRepositoryTransactionRepositorySubscriptionRepositoryPaymentRepositoryMarketplaceRepositoryPublisherRepositoryBlueprintRepositoryContentRepositoryNotificationRepositoryAuditRepository
  • A UI component never calls a repository directly; it calls a service.
  • Every workspace-scoped method takes workspaceId as its first argument.
  • A repository returns Result, never throws a provider error across the boundary.
  • Reads are cursor-paged; unbounded list methods are not part of the contract.

Application services

WorkspaceService
Workspace lifecycle, membership and roles.
ProjectService
Project creation, website ownership and archiving.
SnapshotService
Snapshot requests, versioning and the UWO.
ReportService
Report generation, findings and exports.
CreditService
Estimate, reserve, meter, settle and refund.
BillingService
Checkout, subscriptions, invoices and reconciliation.
MarketplaceService
Listing, review, install and update flows.
LearningService
Paths, lessons, practice and certification.
ExtensionService
Install, permissions, lifecycle and capability grants.
ConnectorService
Connection, credential references and sync runs.
MissionService
Missions derived from findings and workspace state.
  • A component never composes two repositories; that composition is a service.
  • A service emits an audit event for every state change it commits.
  • A service reserves credits before work starts and settles or refunds after.
  • A service returns domain objects and normalized errors only.

Runtime

Configuration, flags, errors and observability

Environment is parsed once into a typed object; failures are normalized; logs and audit events carry the same fields everywhere.

Configuration

APP_ENV
development
DATABASE_PROVIDER
mock
AUTH_PROVIDER
mock
STORAGE_PROVIDER
mock
PAYMENT_PROVIDER
mock
AI_PROVIDER
mock
QUEUE_PROVIDER
mock
CACHE_PROVIDER
mock
SEARCH_PROVIDER
mock
NOTIFICATION_PROVIDER
mock

Defaults are mock everywhere: Marvia runs with no production infrastructure.

Feature flags

ENABLE_MARKETPLACEENABLE_AIENABLE_LEARNINGENABLE_BILLINGENABLE_CONNECTORSENABLE_CREATOR_PLATFORM

Scopes

globalenvironmentworkspaceuserextensionbetainternal

Normalized errors

AuthenticationErrorAuthorizationErrorValidationErrorNotFoundErrorConflictErrorProviderErrorPaymentErrorCreditErrorRateLimitErrorExternalServiceErrorConfigurationError

A provider error is translated at the adapter. The UI never renders a vendor message.

Observability

debug()info()warn()error()audit()

Audit events

workspace.createdproject.createdsnapshot.createdreport.generatedcredits.usedpayment.completedextension.installedconnector.connecteduser.invited

Data flow and security

One path in, one path out

Every feature follows the same route from click to infrastructure, and no credential crosses into the browser.

Target flow

  1. UI
  2. Application Service
  3. Domain
  4. Repository / Provider interface
  5. Adapter
  6. Infrastructure

Worked example — analyze website

  1. Analyze Website
  2. ProjectService
  3. SnapshotService
  4. SnapshotRepository
  5. Queue provider
  6. Runtime worker
  7. Snapshot JSON
  8. Repository
  9. Report Engine

Deployment

  1. Frontend
  2. Cloudflare Workers
  3. Application services
  4. Provider adapters
  5. PostgreSQL / R2 / Queues / KV

Security boundaries

  • Database service role keys never leave the server boundary.
  • Private API keys and AI provider secrets are resolved inside handlers only.
  • Payment secrets and webhook signatures are verified server-side before any write.
  • OAuth client secrets and storage credentials live in the vault, referenced by id.
  • Logging redacts every credential-shaped key: password, token, accessToken, refreshToken, apiKey, secret…

External APIs

  • A connector never returns a raw vendor payload to the application.
  • Credentials are vault references resolved server-side, never passed to the browser.
  • Rate limits and retries belong to the connector, not to the caller.
  • Every external failure is translated into ExternalServiceError.

Related

Where this layer is used

The provider layer is invisible in the product surfaces — that is the point.