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
- Marvia UI→
- Application services→
- Domain→
- Repository and provider interfaces→
- Adapter layer→
- 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.
- 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.
- 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.
- 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.
- Active
- Telegram manual, Paddle, Duitku, Mock
- Planned
- Stripe, PayPal, Midtrans, Xendit
ai
AIOS composes context and prompts; the provider only executes and reports usage.
- 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.
- Active
- Cloudflare Queues, PostgreSQL job queue, Mock
- Planned
- External scheduler
cache
Cache is an accelerator. A miss is always answerable from the repository.
- Active
- Cloudflare KV, In-memory, Mock
- Planned
- Redis, Upstash
search
Search results are domain documents with a score, never rows from a vendor index.
- 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.
- 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
- — 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
- — 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
Scopes
Normalized errors
A provider error is translated at the adapter. The UI never renders a vendor message.
Observability
Audit events
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
- UI→
- Application Service→
- Domain→
- Repository / Provider interface→
- Adapter→
- Infrastructure
Worked example — analyze website
- Analyze Website→
- ProjectService→
- SnapshotService→
- SnapshotRepository→
- Queue provider→
- Runtime worker→
- Snapshot JSON→
- Repository→
- Report Engine
Deployment
- Frontend→
- Cloudflare Workers→
- Application services→
- Provider adapters→
- 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.