oCoreoCore Docs

Service Layer

Detailed documentation of oCore's service layer, responsibilities, dependencies, and patterns

The service layer is the core of oCore's backend. Services encapsulate all business logic, orchestrate database queries through Ent, interact with external systems (SSH, DNS, email, Git), and enqueue background jobs via River. Handlers never contain business logic -- they delegate to services.

Service Inventory

oCore's service layer consists of 50+ services organized by domain. The table below lists the major services and their responsibilities.

Authentication and Authorization

ServiceFileResponsibility
AuthServiceauth.goSignup, login, email verification, password reset, OAuth
TokenServicetoken.goJWT access/refresh token generation and validation
TOTPServicetotp.goTOTP two-factor authentication setup and verification
WebAuthnServicewebauthn.goPasskey/WebAuthn registration and authentication
RBACServicerbac.goRole-based permission checking
OAuthServiceoauth.goOAuth provider integration (GitHub, Google)
PasswordResetServicepasswordreset.goPassword reset token generation and redemption
VerificationServiceverification.goEmail verification token management

Organization Management

ServiceFileResponsibility
OrgServiceorg.goOrganization CRUD, slug generation, system role seeding
OrgSettingsServiceorg_settings.goOrganization-level settings management
InvitationServiceinvitation.goMember invitations with email and token
AgencyServiceagency.goAgency-client organization relationships
APIKeyServiceapi_key.goAPI key generation, hashing, and scope management

Infrastructure Management

ServiceFileResponsibility
ServerServiceserver.goServer CRUD, SSH connectivity validation
ProvisionerServiceprovisioner.goServer provisioning automation
InstanceServiceinstance.goOdoo instance lifecycle management
InstanceConfigServiceinstance_config.goInstance configuration (odoo.conf) management
InstanceComposeServiceinstance_compose.goDocker Compose generation for instances
PortAllocatorServiceport_allocator.goPort allocation (base 10000, gap of 10)
CapacityServicecapacity.goServer capacity monitoring and planning

Deployment Pipeline

ServiceFileResponsibility
ProjectServiceproject.goGit project management and repository linking
EnvironmentServiceenvironment.goEnvironment lifecycle (staging, production)
DeployServicedeploy.goDeployment orchestration and rollback
BuildServicebuild.goInstance build pipeline (Docker builds)
CloneServiceclone.goEnvironment cloning with data
PromotionServicepromotion.goEnvironment promotion (staging to production)
TransferServicetransfer.goCross-server transfer orchestration

Database Management

ServiceFileResponsibility
DatabaseServicedatabase.goPostgreSQL database lifecycle management
DatabaseMaintenanceServicedatabase_maintenance.goVACUUM, ANALYZE, REINDEX operations
DatabaseTuningServicedatabase_tuning.goAuto-tuning based on server resources
DatabaseMetricsServicedatabase_metrics.goQuery performance metrics collection
DatabaseReplicasServicedatabase_replicas.goRead replica setup and management
ImportExportServicedb_import_export.goDatabase import/export operations

Backup and Recovery

ServiceFileResponsibility
BackupServicebackup.goBackup creation, scheduling, and management
BackupDestinationServicebackup_destination.goS3/local backup destination configuration
BackupRestoreServicebackup_restore.goBackup restoration pipeline
SnapshotServicesnapshot.goFilesystem snapshots for instant rollback
DisasterRecoveryServicedisaster_recovery.goDisaster recovery planning and execution

Monitoring and Observability

ServiceFileResponsibility
MonitoringServicemonitoring.goServer and instance health monitoring
MetricsServicemetrics.goMetrics aggregation and querying
AlertServicealert.goAlert rule evaluation and notification
UptimeServiceuptime.goUptime tracking and SLA reporting
LogStreamServicelog_stream.goReal-time log streaming via SSE/WebSocket
AuditServiceaudit.goAudit trail recording

Networking

ServiceFileResponsibility
DomainServicedomain.goCustom domain configuration and SSL
DNSConfigServicedns_config.goDNS provider integration
ProxyManagerServiceproxy_manager.goReverse proxy management (Nginx, Traefik, NPM)
HAProxyServicehaproxy.goHAProxy load balancer configuration
IPAccessServiceip_access.goIP whitelist/blacklist rules

Service Dependencies

Services depend on each other and on shared infrastructure. Dependencies are injected through constructors.

Loading diagram...

Common Dependencies

Most services depend on:

  • *ent.Client -- Database access through the Ent ORM
  • *river.Client[pgx.Tx] -- Job queue for async operations (services that trigger background work)
  • Other services when business logic spans multiple domains

Transaction Patterns

Single-Entity Transactions

Simple CRUD operations use Ent's built-in transactional methods:

func (s *OrgService) CreateOrganization(ctx context.Context, name, slug string, creatorID uuid.UUID) (*ent.Organization, error) {
    // Ent handles the transaction internally
    org, err := s.client.Organization.Create().
        SetName(name).
        SetSlug(slug).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("create organization: %w", err)
    }

    // Seed system roles within the same request context
    if err := s.seedSystemRoles(ctx, org.ID); err != nil {
        return nil, fmt.Errorf("seed roles: %w", err)
    }

    return org, nil
}

Multi-Entity Transactions

When multiple entities need to be created atomically, services use Ent's explicit transaction API:

func (s *EnvironmentService) CreateEnvironment(ctx context.Context, req CreateEnvRequest) (*ent.Environment, error) {
    tx, err := s.client.Tx(ctx)
    if err != nil {
        return nil, fmt.Errorf("start transaction: %w", err)
    }
    defer tx.Rollback() // No-op if committed

    env, err := tx.Environment.Create().
        SetName(req.Name).
        SetProjectID(req.ProjectID).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("create environment: %w", err)
    }

    _, err = tx.DatabaseConfig.Create().
        SetEnvironmentID(env.ID).
        SetMaxConnections(100).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("create db config: %w", err)
    }

    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("commit: %w", err)
    }

    return env, nil
}

Transactional Job Enqueue

Jobs can be enqueued within a database transaction, ensuring the job is only created if the transaction commits:

func (s *ServerService) DeleteServer(ctx context.Context, tx *ent.Tx, serverID uuid.UUID) error {
    // Mark as removing within the transaction
    _, err := tx.Server.UpdateOneID(serverID).
        SetStatus("removing").
        Save(ctx)

    // Enqueue cleanup job -- only persisted if tx commits
    _, err = s.riverClient.InsertTx(ctx, tx, jobargs.CleanupServerArgs{
        ServerID: serverID,
    }, nil)

    return nil
}

Error Handling Patterns

Services use wrapped errors with fmt.Errorf for context propagation:

// Always wrap errors with context
if err != nil {
    return nil, fmt.Errorf("list servers for org %s: %w", orgID, err)
}

Handlers check error types to determine the appropriate HTTP status:

  • ent.IsNotFound(err) --> 404 Not Found
  • ent.IsConstraintError(err) --> 409 Conflict (duplicate key, FK violation)
  • ent.IsValidationError(err) --> 400 Bad Request
  • Custom domain errors --> mapped to specific status codes

Never return raw database errors to the client. Always wrap errors with domain context and let the handler map to user-friendly messages.

Adding a New Service

  1. Create a file in internal/service/ following the naming convention
  2. Define a struct with dependencies as fields
  3. Create a constructor function (NewXxxService)
  4. Implement methods with the context pattern: (ctx context.Context, ...) (..., error)
  5. Wire the service in cmd/server/main.go
  6. Write unit tests in internal/service/xxx_test.go

Further Reading

Was this page helpful?