404 Not Found

404 Not Found


nginx

Design do Projeto — Arquitetura da Plataforma SaaS ShopMetrics

Programar sem design é como construir uma casa sem planta — você só percebe que a fundação não é profunda o suficiente quando já chegou ao terceiro andar, momento em que não tem outra opção senão demolir e recomeçar.

1. O Que Você Vai Aprender


2. Uma História Real de Construção de um Negócio SaaS do Zero

(1) Dor: Bob quer construir uma plataforma de analytics de e-commerce mas não sabe por onde começar

Bob dirige uma empresa de consultoria de dados de e-commerce com mais de 50 clientes. Os dados de cada cliente estão espalhados por oito plataformas de e-commerce, e Bob os agrega manualmente no Excel — gastando quatro horas por dia criando relatórios, que frequentemente estão cheios de erros. Alice, a gerente de operações de um desses clientes, diz: "Seria ótimo ter um dashboard que mostrasse todos os dados das lojas em tempo real." Charlie, o analista de dados de Bob, diz: "Precisamos de uma plataforma SaaS multi-tenant, mas como devemos projetar a arquitetura?"

(2) Abordagem Sistemática de Design

Comece com análise de requisitos → user stories → seleção de arquitetura → modelagem de dados → design de API → seleção de tecnologia. Cada passo tem entregáveis claros, e você segue a planta ao programar.

TEXT
Análise de Requisitos → User Stories → Seleção de Arquitetura → Design ER → Design de API → Seleção de Tecnologia → Começar a Programar

(3) Resultado

Depois que Bob gastou duas semanas completando o design, sua eficiência de programação triplicou — porque cada requisito tinha uma interface clara e design de banco de dados, eliminando a necessidade de fazer alterações ao longo do caminho.


3. Análise de Requisitos e User Stories

(1) Três Tipos de Perfis de Usuário

Papel Nome Necessidade Principal Ações Típicas
Administradora de Tenant Alice Gerenciar sua própria loja e equipe Criar lojas, convidar membros, ver dashboard
Operações da Plataforma Bob Gerenciar todos os tenants e cobranças Aprovar tenants, gerenciar planos, ver estatísticas da plataforma
Analista de Dados Charlie Analisar dados de e-commerce e gerar relatórios Criar relatórios, configurar alertas e exportar dados

(2) User Stories

TEXT
Como Alice (Admin de Tenant), eu quero:
- US-01: Adicionar uma nova loja de e-commerce para acompanhar suas métricas
- US-02: Convidar membros da equipe para que Charlie possa acessar analytics
- US-03: Ver um dashboard mostrando receita/pedidos em todas as minhas lojas
- US-04: Assinar um plano que se adapte ao meu número de lojas
- US-05: Exportar relatórios em formato CSV/Excel
- US-06: Configurar alertas quando a receita cair abaixo de um limite

Como Bob (Operador da Plataforma), eu quero:
- US-07: Gerenciar contas de tenants (criar/suspender/excluir)
- US-08: Definir planos de assinatura com diferentes limites de funcionalidades
- US-09: Ver métricas de toda a plataforma (total de tenants/MRR/ativações)
- US-10: Processar pagamentos de assinatura via Stripe
- US-11: Enviar notificações aos tenants sobre assinaturas expirando

Como Charlie (Analista de Dados), eu quero:
- US-12: Construir relatórios de analytics personalizados com intervalo de datas e filtros
- US-13: Comparar performance de lojas lado a lado
- US-14: Agendar geração automática de relatórios (diário/semanal/mensal)
- US-15: Receber notificações em tempo real quando eventos significativos ocorrerem

(1) ▶ Exemplo: Decompondo User Stories em Módulos Funcionais

PHP
// User stories → Mapeamento de módulos funcionais
return [
    'Gerenciamento de Tenant' => [
        'US-07: Gerenciar contas de tenants',
        'US-01: Adicionar lojas de e-commerce',
        'US-02: Convidar membros da equipe',
    ],
    'Assinatura e Cobrança' => [
        'US-04: Assinar um plano',
        'US-08: Definir planos de assinatura',
        'US-10: Processar pagamentos Stripe',
        'US-11: Notificações de expiração',
    ],
    'Analytics e Relatórios' => [
        'US-03: Ver dashboard de receita',
        'US-05: Exportar relatórios',
        'US-12: Construir relatórios personalizados',
        'US-13: Comparar performance de lojas',
        'US-14: Agendar geração de relatórios',
    ],
    'Alertas e Notificações' => [
        'US-06: Alertas de queda de receita',
        'US-15: Notificações de eventos em tempo real',
    ],
    'Administração da Plataforma' => [
        'US-09: Métricas de toda a plataforma',
    ],
];

Saída:

TEXT
// Execução bem-sucedida

4. Selecionando uma Arquitetura Multitenant

(1) Três Estratégias de Multitenancy

Estratégia Nível de Isolamento Custo Complexidade Casos de Uso
Banco de Dados Independente Mais alto Alto Médio Requisitos de Conformidade Financeira/Saúde
Banco Compartilhado + Schema Independente Médio Médio Médio Médio porte, com alguns requisitos de isolamento
Banco Compartilhado + Schema Compartilhado Mais baixo Baixo Baixo Maioria dos SaaS, isolamento por tenant_id

(2) Decisão de Seleção do ShopMetrics

100%
flowchart TD
    A[Estratégia Multi-tenant] --> B{Requisito de Isolamento de Dados?}
    B -->|Conformidade rigorosa| C[BD Separado por Tenant]
    B -->|SaaS padrão| D{Quantidade de Tenants?}
    D -->|< 100| E[BD Compartilhado + Schema Separado]
    D -->|> 100| F[BD Compartilhado + Schema Compartilhado]
    F --> G[tenant_id em cada linha]
    G --> H[Global Scope com auto-filtro]
    H --> I[Escolha do ShopMetrics ✅]

O ShopMetrics escolheu a estratégia de banco compartilhado + schema compartilhado:

(1) ▶ Exemplo: Implementação de Global Scope Multi-tenant

PHP
// app/Models/Traits/BelongsToTenant.php
trait BelongsToTenant
{
    protected static function bootBelongsToTenant(): void
    {
        static::addGlobalScope('tenant', function (Builder $builder) {
            $tenantId = Tenant::current()?->id;
            if ($tenantId) {
                $builder->where('tenant_id', $tenantId);
            }
        });

        static::creating(function (Model $model) {
            $tenantId = Tenant::current()?->id;
            if ($tenantId && ! $model->isDirty('tenant_id')) {
                $model->tenant_id = $tenantId;
            }
        });
    }
}

// app/Models/Tenant.php
class Tenant extends Model
{
    protected static Tenant $currentTenant;

    public static function setCurrent(self $tenant): void
    {
        static::$currentTenant = $tenant;
    }

    public static function current(): ?self
    {
        return static::$currentTenant ?? null;
    }
}

// app/Http/Middleware/SetTenantContext.php
class SetTenantContext
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($user = $request->user()) {
            Tenant::setCurrent($user->tenant);
        }
        return $next($request);
    }
}

Saída:

TEXT
// Execução bem-sucedida

5. Design ER de Banco de Dados

(1) Relacionamentos de Entidades Principais

100%
erDiagram
    TENANT ||--o{ USER : "tem muitos"
    TENANT ||--o{ SHOP : "tem muitos"
    TENANT ||--|| SUBSCRIPTION : "tem um"
    PLAN ||--o{ SUBSCRIPTION : "assinado por"
    SHOP ||--o{ ORDER : "tem muitos"
    SHOP ||--o{ PRODUCT : "tem muitos"
    ORDER ||--|{ ORDER_ITEM : "contém"
    ORDER_ITEM }o--|| PRODUCT : "referencia"
    USER ||--o{ REPORT : "cria"
    TENANT ||--o{ ALERT : "configura"

    TENANT {
        bigint id PK
        string name
        string slug UK
        string domain
        string status
        timestamp created_at
    }
    USER {
        bigint id PK
        bigint tenant_id FK
        string name
        string email UK
        string role
        timestamp created_at
    }
    PLAN {
        bigint id PK
        string name
        string slug UK
        int shop_limit
        int user_limit
        int price_cents
        string stripe_price_id
    }
    SUBSCRIPTION {
        bigint id PK
        bigint tenant_id FK
        bigint plan_id FK
        string stripe_id
        string status
        timestamp trial_ends_at
        timestamp ends_at
    }
    SHOP {
        bigint id PK
        bigint tenant_id FK
        string name
        string platform
        string external_id
        string status
    }
    ORDER {
        bigint id PK
        bigint tenant_id FK
        bigint shop_id FK
        string external_id
        string customer_email
        int total_cents
        string status
        timestamp ordered_at
    }
    PRODUCT {
        bigint id PK
        bigint tenant_id FK
        bigint shop_id FK
        string name
        string sku
        int price_cents
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        int unit_price_cents
    }
    REPORT {
        bigint id PK
        bigint tenant_id FK
        bigint user_id FK
        string type
        string format
        string status
        string storage_path
        timestamp generated_at
    }
    ALERT {
        bigint id PK
        bigint tenant_id FK
        string type
        string condition
        string channel
        boolean is_active
    }

(2) Decisões Chave de Design

Decisão Escolha Motivo
Armazenamento de Valores int price_cents Evitar problemas de precisão de ponto flutuante
Isolamento de Tenant tenant_id em cada tabela Estratégia de Schema Compartilhado
Status da Assinatura Sincronização Stripe Webhook Fonte Única de Dados (Stripe)
ID Externo external_id UK por tenant Diferentes formatos de ID de plataformas de e-commerce
Soft Delete Apenas User/Tenant Não excluir pedidos/produtos; apenas alterar status

(1) ▶ Exemplo: Definição dos Models Principais do ShopMetrics

PHP
// app/Models/Tenant.php
class Tenant extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['name', 'slug', 'domain', 'status'];

    protected static function booted(): void
    {
        static::creating(function (self $tenant) {
            $tenant->slug ??= Str::slug($tenant->name);
            $tenant->domain ??= "{$tenant->slug}.shopmetrics.io";
        });
    }

    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }

    public function shops(): HasMany
    {
        return $this->hasMany(Shop::class);
    }

    public function subscription(): HasOne
    {
        return $this->hasOne(Subscription::class)->ofMany([], fn ($q) => $q->orderByDesc('created_at'));
    }

    public function alerts(): HasMany
    {
        return $this->hasMany(Alert::class);
    }

    public function isActive(): bool
    {
        return $this->status === 'active' &&
               $this->subscription?->stripe_status === 'active';
    }

    public function canAddShop(): bool
    {
        $limit = $this->subscription?->plan->shop_limit ?? 0;
        return $this->shops()->count() < $limit;
    }
}

// app/Models/Order.php
class Order extends Model
{
    use BelongsToTenant, HasFactory;

    protected $fillable = [
        'tenant_id', 'shop_id', 'external_id',
        'customer_email', 'total_cents', 'status', 'ordered_at',
    ];

    protected $casts = [
        'total_cents' => 'integer',
        'ordered_at' => 'datetime',
    ];

    public function shop(): BelongsTo
    {
        return $this->belongsTo(Shop::class);
    }

    public function items(): HasMany
    {
        return $this->hasMany(OrderItem::class);
    }

    public function getTotalDollarsAttribute(): float
    {
        return $this->total_cents / 100;
    }
}

Saída:

TEXT
// Execução bem-sucedida

6. Design de API

(1) Versionamento e Planejamento de Recursos API

Recurso Prefixo Método Descrição
Auth /api/v1/auth POST login/logout/refresh Autenticação
Tenants /api/v1/tenants GET/PATCH current Informações do Tenant
Shops /api/v1/shops CRUD Gerenciamento de Lojas
Orders /api/v1/shops/{id}/orders GET/POST Consulta de Pedidos
Products /api/v1/shops/{id}/products GET/POST Gerenciamento de Produtos
Dashboard /api/v1/dashboard GET overview/top/revenue Dashboard
Reports /api/v1/reports POST generate/GET status Relatórios
Alerts /api/v1/alerts CRUD Configuração de Alertas
Plans /api/v1/plans GET list Lista de Planos
Subscriptions /api/v1/subscriptions POST/DELETE Gerenciamento de Assinaturas

(2) Especificação de Resposta API

JSON
{
    "data": {
        "id": 1,
        "type": "shop",
        "attributes": {
            "name": "Loja Amazon da Alice",
            "platform": "amazon",
            "status": "active",
            "created_at": "2024-01-15T10:00:00Z"
        },
        "relationships": {
            "tenant": { "data": { "id": 1, "type": "tenant" } }
        }
    },
    "meta": {
        "request_id": "req_abc123",
        "timestamp": "2024-03-15T14:30:00Z"
    }
}

(1) ▶ Exemplo: Design de Rotas API do ShopMetrics

PHP
// routes/api.php
Route::prefix('v1')->group(function () {

    // Público: Autenticação
    Route::post('auth/login', [AuthController::class, 'login']);
    Route::post('auth/register', [AuthController::class, 'register']);

    // Rotas autenticadas
    Route::middleware(['auth:sanctum', 'set-tenant-context'])->group(function () {

        // Auth
        Route::post('auth/logout', [AuthController::class, 'logout']);
        Route::get('auth/me', [AuthController::class, 'me']);

        // Tenant (tenant do usuário atual)
        Route::get('tenant', [TenantController::class, 'show']);
        Route::patch('tenant', [TenantController::class, 'update']);

        // Lojas
        Route::apiResource('shops', ShopController::class);

        // Recursos aninhados sob lojas
        Route::prefix('shops/{shop}')->group(function () {
            Route::apiResource('orders', OrderController::class)->only(['index', 'show']);
            Route::apiResource('products', ProductController::class);
        });

        // Dashboard
        Route::prefix('dashboard')->group(function () {
            Route::get('overview', [DashboardController::class, 'overview']);
            Route::get('top-products', [DashboardController::class, 'topProducts']);
            Route::get('revenue', [DashboardController::class, 'revenueChart']);
        });

        // Relatórios
        Route::apiResource('reports', ReportController::class)->only(['index', 'store', 'show']);
        Route::post('reports/{report}/download', [ReportController::class, 'download']);

        // Alertas
        Route::apiResource('alerts', AlertController::class);

        // Assinatura
        Route::get('plans', [PlanController::class, 'index']);
        Route::post('subscriptions', [SubscriptionController::class, 'store']);
        Route::get('subscription', [SubscriptionController::class, 'show']);
        Route::delete('subscription', [SubscriptionController::class, 'cancel']);

        // Gerenciamento de equipe (apenas tenant_owner)
        Route::middleware('role:tenant_owner')->prefix('team')->group(function () {
            Route::get('members', [TeamController::class, 'index']);
            Route::post('invite', [TeamController::class, 'invite']);
            Route::delete('members/{user}', [TeamController::class, 'remove']);
        });
    });

    // Stripe Webhooks (sem auth)
    Route::post('webhooks/stripe', [WebhookController::class, 'handleStripe']);
});

Saída:

TEXT
// Execução bem-sucedida

7. Decisões de Seleção de Tecnologia

(1) Comparação e Seleção do Stack de Tecnologia

Camada Opção Seleção Motivo
Framework Laravel/Symfony/Lumen Laravel 11 Completo, ecossistema rico, desenvolvimento SaaS rápido
Banco de Dados MySQL/PostgreSQL MySQL 8.0 Familiar para a equipe, padrão do Laravel, suporta JSON
Cache Redis/Memcached Redis 7 Cache, Sessions, Filas e Broadcasting unificados
Armazenamento S3/MinIO/Local S3 Escalável, integração CDN, MinIO para desenvolvimento
Fila Redis/Database/SQS Redis Baixa latência, ambientes de desenvolvimento e produção unificados
Autenticação Sanctum/Passport Sanctum Autenticação Token SPA + Mobile é suficiente
Tempo real Pusher/Soketi Soketi Compatível com protocolo Pusher; auto-hospedado sem custos
Front-end Blade/Inertia/Livewire Inertia + Vue Experiência SPA + Roteamento Server-Side

(2) Visão Geral da Arquitetura

100%
flowchart TB
    subgraph Client["Camada de Cliente"]
        WEB[Web SPA - Inertia/Vue]
        MOBILE[App Mobile]
        API_CLIENT[Consumidores API]
    end

    subgraph LB["Load Balancer - Nginx"]
        direction LR
        direction TB
    end

    subgraph App["Camada de Aplicação"]
        API[Servidor API - Laravel]
        WS[WebSocket - Soketi]
    end

    subgraph Worker["Processamento em Segundo Plano"]
        QUEUE[Workers de Fila]
        SCHED[Agendador]
    end

    subgraph Data["Camada de Dados"]
        DB[(MySQL 8.0)]
        CACHE[(Redis 7)]
        S3[S3/MinIO]
    end

    subgraph External["Serviços Externos"]
        STRIPE[Stripe API]
        SHOPS[APIs de E-commerce]
        MAIL[Serviço de Email]
    end

    WEB --> LB
    MOBILE --> LB
    API_CLIENT --> LB
    LB --> API
    WEB --> WS

    API --> DB
    API --> CACHE
    API --> S3
    API --> STRIPE
    API --> SHOPS
    API --> QUEUE
    API --> WS

    QUEUE --> DB
    QUEUE --> CACHE
    QUEUE --> S3
    QUEUE --> MAIL
    QUEUE --> STRIPE
    SCHED --> QUEUE

(1) ▶ Exemplo: Perfil de Configuração de Seleção de Tecnologia do ShopMetrics

PHP
// config/shopmetrics.php
return [
    'tenant' => [
        'strategy' => env('TENANT_STRATEGY', 'shared_schema'),
        'default_plan' => env('DEFAULT_PLAN_SLUG', 'starter'),
        'trial_days' => env('TRIAL_DAYS', 14),
    ],

    'limits' => [
        'starter' => ['shops' => 3, 'users' => 5, 'reports_per_month' => 10],
        'pro' => ['shops' => 25, 'users' => 25, 'reports_per_month' => 100],
        'enterprise' => ['shops' => -1, 'users' => -1, 'reports_per_month' => -1],
    ],

    'reports' => [
        'max_date_range_days' => 365,
        'formats' => ['csv', 'xlsx', 'json'],
        'storage_disk' => env('REPORT_DISK', 's3'),
        'retention_days' => env('REPORT_RETENTION_DAYS', 90),
    ],

    'sync' => [
        'platforms' => ['amazon', 'shopify', 'ebay', 'etsy'],
        'sync_interval_minutes' => env('SYNC_INTERVAL', 60),
        'batch_size' => env('SYNC_BATCH_SIZE', 500),
    ],

    'alerts' => [
        'channels' => ['email', 'slack', 'webhook'],
        'check_interval_minutes' => env('ALERT_CHECK_INTERVAL', 15),
    ],
];

Saída:

TEXT
// Execução bem-sucedida

8. Exemplo Compreensivo: Inicializando o Projeto ShopMetrics

PHP
// ============================================
// Compreensivo: Scaffold do projeto e configuração inicial
// Migrations do banco de dados, models e config
// ============================================

// database/migrations/2024_01_01_000001_create_tenants_table.php
Schema::create('tenants', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('domain')->unique();
    $table->string('status')->default('active'); // active/suspended/cancelled
    $table->softDeletes();
    $table->timestamps();

    $table->index('status');
});

// database/migrations/2024_01_01_000002_create_plans_table.php
Schema::create('plans', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->integer('shop_limit')->default(3);
    $table->integer('user_limit')->default(5);
    $table->integer('price_cents')->default(0);
    $table->string('stripe_price_id')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

// database/migrations/2024_01_01_000003_create_subscriptions_table.php
Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->foreignId('plan_id')->constrained();
    $table->string('stripe_id')->unique();
    $table->string('stripe_status');
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();

    $table->index(['tenant_id', 'stripe_status']);
});

// database/migrations/2024_01_01_000004_create_shops_table.php
Schema::create('shops', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->string('platform'); // amazon/shopify/ebay/etsy
    $table->string('external_id');
    $table->string('status')->default('active');
    $table->json('metadata')->nullable();
    $table->timestamps();

    $table->unique(['tenant_id', 'platform', 'external_id']);
    $table->index(['tenant_id', 'status']);
});

// database/migrations/2024_01_01_000005_create_orders_table.php
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->foreignId('shop_id')->constrained()->cascadeOnDelete();
    $table->string('external_id');
    $table->string('customer_email')->nullable();
    $table->unsignedBigInteger('total_cents')->default(0);
    $table->string('status')->default('pending');
    $table->timestamp('ordered_at');
    $table->timestamps();

    $table->unique(['tenant_id', 'external_id']);
    $table->index(['tenant_id', 'shop_id', 'status', 'ordered_at']);
    $table->index(['tenant_id', 'ordered_at']);
});

❓ Perguntas Frequentes

P O isolamento de tenant com schema compartilhado é seguro o suficiente?
R É suficiente para a maioria das aplicações SaaS. Medidas-chave: Global Scope filtra automaticamente tenant_id; a camada API valida o tenant ao qual o usuário atual pertence; e SQL bruto é impedido de bypassar o Scope. Para isolamento superior (ex.: financeiro ou saúde), use a estratégia de banco de dados separado.
P Quando devem ser usadas associações polimórficas no design ER?
R Associações polimórficas são adequadas para cenários onde "uma única entidade pode pertencer a múltiplas entidades pai" (ex.: um Comment pode pertencer a um Post ou um Video). No ShopMetrics, um Alert pode pertencer a um Shop ou a um Tenant, então associações polimórficas podem ser consideradas. No entanto, se o tipo de associação é fixo, usar uma chave estrangeira proporciona maior clareza.
P Como as versões de API devem ser gerenciadas?
R A abordagem mais simples é usar um prefixo de URL para versionamento (/api/v1/). Quando v1 e v2 coexistem, as rotas v2 são tratadas em um arquivo separado, enquanto as camadas Model e Service são compartilhadas. Um novo número de versão é atribuído apenas para upgrades de versão principal; mudanças menores são implementadas de forma retrocompatível.
P As capacidades da equipe devem ser consideradas na seleção de tecnologias?
R Absolutamente. Mesmo o stack tecnológico mais avançado pode realmente desacelerar as coisas se a equipe não estiver familiarizada. Priorize as tecnologias que a equipe mais domina, seguido pela maturidade do ecossistema e, por último, avanço tecnológico.
P Quão detalhada deve ser a fase de design?
R Entidades e relacionamentos principais devem estar 100% finalizados (diagrama ER, endpoints API); detalhes de implementação podem ser ajustados durante a programação. Objetivo do design: Não deve haver necessidade de "inventar" novas tabelas ou interfaces durante a programação.
P Como lidar com migração de dados em um ambiente multi-tenant?
R É simples para schemas compartilhados — basta adicionar uma coluna tenant_id e criar um script de migração de dados. Ao migrar de single-tenant para multi-tenant, use um comando Artisan para atribuir valores tenant_id em lote; após o processamento, adicione uma restrição NOT NULL.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Com base no diagrama ER, escreva os arquivos de schema para as três tabelas restantes do ShopMetrics (products, order_items, alerts), incluindo os índices e chaves estrangeiras necessários.

  2. Exercício Avançado (⭐⭐): Usando o formato OpenAPI 3.0 YAML, escreva a documentação da API de Orders do ShopMetrics (GET lista / GET detalhe / POST sync), incluindo parâmetros de requisição, formatos de resposta e códigos de erro.

  3. Desafio (⭐⭐⭐): Design uma solução completa de middleware multi-tenant para o ShopMetrics — implemente o middleware SetTenantContext, a trait BelongsToTenant e use parâmetros de caminho (subdomínio) para identificar tenants. Escreva testes para verificar que o Tenant A não pode acessar dados do Tenant B.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%