Skip to main content
Vito Business OS is a multi-tenant platform where each business (called a Tenant) operates in a fully isolated environment. A single Laravel application serves all tenants — their data, routing, and capabilities are separated by the platform.

How routing works

Tenants are identified by their slug field. The routing strategy differs between environments.
In local development, the microsite uses a path-based fallback at /t/{tenant:slug}. In production, you configure subdomain routing (e.g., {tenant}.yourdomain.com) via config/tenancy.php and Nginx. The path-based routes are always registered as a fallback for compatibility.
// routes/web.php — path-based fallback (always active)
Route::get('/t/{tenant:slug}', [TenantMicrositeController::class, 'show']);
Route::get('/t/{tenant:slug}/book', [BookingController::class, 'index']);

Public tenant surfaces

RoutePurpose
/t/{tenant}Public microsite (catalog, SEO, checkout)
/t/{tenant}/bookPublic booking and appointment page
/appointments/{appointment}/manageSigned URL portal for appointment self-management
/registrar-negocioPublic business registration form

The Tenant model

The Tenant model (app/Models/Tenant.php) is the central entity of the platform. It owns all business data and drives plan-gated capabilities.

Core attributes

// app/Models/Tenant.php
protected $fillable = [
    'name', 'slug', 'description', 'address',
    'city_id', 'category_id',
    'whatsapp_number', 'whatsapp_message', 'phone', 'email',
    'website_url', 'facebook_url', 'instagram_url', 'tiktok_url',
    'brand_color', 'slogan', 'seo_title', 'seo_description',
    'tags', 'payment_methods',
    'delivery_enabled', 'pickup_enabled', 'minimum_order',
    'business_type',
    // Premium profile fields (PRO)
    'about', 'hero_image', 'latitude', 'longitude', 'cover_path',
];

What a Tenant owns

Catalog

Products, menu sections, modifier groups, product option groups, and gallery images.

Sales

Orders, order items, order events, payments, and subscription payments.

Bookings

Services, service providers, appointments, and opening hours.

Marketing

Coupons, campaigns, announcements, leads, and follower relationships.

Loyalty

Loyalty program configuration, rewards catalog, and ledger entries.

Analytics

Tenant visits, aggregated daily analytics, and interaction tracking.

Relationships defined on Tenant

// app/Models/Tenant.php
public function products(): HasMany { ... }
public function menuSections(): HasMany { ... }
public function payments(): HasMany { ... }
public function leads(): HasMany { ... }
public function reviews(): HasMany { ... }
public function tenantVisits(): HasMany { ... }
public function campaigns(): HasMany { ... }
public function coupons(): HasMany { ... }
public function openingHours(): HasMany { ... }
public function services(): HasMany { ... }
public function serviceProviders(): HasMany { ... }
public function appointments(): HasMany { ... }
public function followers() { ... } // BelongsToMany User via 'follows'

Tenant isolation

Data isolation is enforced at two layers.

BelongsToTenant trait

All tenant-owned models use the BelongsToTenant trait (app/Infrastructure/Persistence/Eloquent/Traits/BelongsToTenant.php). It automatically scopes every Eloquent query to the current authenticated tenant and sets tenant_id on creation.
// Example: Product is always scoped to the current tenant
class Product extends Model
{
    use BelongsToTenant; // All queries: WHERE tenant_id = ?

    protected $guarded = [
        'id',
        'tenant_id', // IMMUTABLE OWNERSHIP
    ];
}

Route-level middleware

API routes protected by tenant.ownership middleware verify the authenticated user’s tenant matches the requested resource before any controller logic executes.
// config reference: API routes use auth:sanctum + tenant.ownership
// routes/api.php
Route::middleware(['auth:sanctum', 'tenant.ownership'])->group(function () {
    Route::get('/products', ...);
    Route::apiResource('/orders', ...);
});

Tenant creation flow

1

Public registration

A business owner visits /registrar-negocio. The React/Inertia form is served by RegisterTenantController@create. The POST endpoint is rate-limited to 5 attempts per minute.
// routes/web.php
Route::get('/registrar-negocio', 'create')->name('tenant.register');
Route::post('/registrar-negocio', 'store')
    ->middleware('throttle:5,1')
    ->name('tenant.register.store');
2

Slug uniqueness check

The UI calls GET /api/check-slug before submission to verify slug availability without full form submission.
3

Atomic registration

RegisterTenantController@store creates the User and Tenant records in a single database transaction. The tenant starts on the semilla plan.
4

Payment (optional)

For paid plans, the owner is redirected to /registro/pago-pendiente/{payment} to monitor payment confirmation status.
5

Workspace access

After registration, the owner accesses the operational workspace at /workspace/dashboard or the Filament panel at /app.

Plan system

// app/Models/Tenant.php
public const PLAN_SEMILLA = 'semilla';     // Free tier
public const PLAN_CRECIMIENTO = 'crecimiento'; // Growth tier
public const PLAN_COSECHA = 'cosecha';    // Harvest tier

// Computed: is_pro is true when plan_type is 'pro' or 'enterprise'
// and subscription_status is 'active' or 'trial'
public function getIsProAttribute(): bool
{
    $value = $this->plan_type->value;
    if (!in_array($value, ['pro', 'enterprise'])) return false;
    if (in_array($this->subscription_status, ['past_due', 'canceled'])) return false;
    return $this->subscription_ends_at === null || $this->subscription_ends_at->isFuture();
}

Subscription statuses

StatusLabelCan use PRO features
activeActivoYes
trialPeríodo de PruebaYes
past_dueVencidoNo
canceledCanceladoNo

Capability gating

Feature flags are evaluated at runtime through the CapabilityManagerInterface:
// app/Models/Tenant.php
public function hasCapability(Capability $capability): bool
{
    return app(CapabilityManagerInterface::class)->has($this, $capability);
}
The getPublicMenuStructure() method is an example of plan-gated data access — it returns null for non-pro tenants, triggering the fallback flat product list.

Media collections

The Tenant model registers three Spatie Medialibrary collections:
// app/Models/Tenant.php
public function registerMediaCollections(): void
{
    $this->addMediaCollection('logo')->singleFile()->useDisk('public');
    $this->addMediaCollection('cover')->singleFile()->useDisk('public');
    $this->addMediaCollection('hero')->singleFile()->useDisk('public');
}
Conversions registered: thumb (400×400) and optimized (800px WebP at 80% quality).

Activity logging

The Tenant model uses Spatie Activitylog. It logs changes to name, is_active, plan_type, and subscription_status under the negocios log name, with dirty-only tracking to suppress no-op updates.

Build docs developers (and LLMs) love