[{"data":1,"prerenderedAt":3340},["ShallowReactive",2],{"blog-nuxt-saas-multitenant":3},{"id":4,"title":5,"author":6,"body":7,"date":3323,"description":3324,"extension":3325,"image":3326,"meta":3327,"navigation":761,"path":3328,"seo":3329,"stem":3330,"tags":3331,"__hash__":3339},"blog\u002Fblog\u002Fnuxt-saas-multitenant.md","Como estruturei a arquitetura inicial de um SaaS multitenant com Nuxt 3 e Supabase","Larissa Santos",{"type":8,"value":9,"toc":3303},"minimark",[10,19,54,57,102,109,112,117,120,127,130,146,149,152,154,158,161,211,217,220,266,273,279,345,350,369,372,399,407,409,413,419,422,482,489,497,503,506,521,527,536,542,544,548,554,557,587,594,597,606,609,635,638,641,647,649,653,656,662,665,775,782,788,1280,1286,1303,1310,1312,1316,1319,1322,1563,1574,1581,1583,1589,1592,1864,1874,1877,1972,1975,1977,1981,1989,1991,2037,2040,2042,2046,2049,2055,2594,2597,2599,2606,2609,2780,2786,2803,2806,2808,2845,2848,2901,2904,2907,2909,2913,2916,3020,3023,3025,3029,3120,3123,3136,3147,3149,3153,3214,3216,3220,3226,3245,3248,3251,3255,3261,3264,3266,3270,3299],[11,12,13,14,18],"p",{},"Recentemente, precisei arquitetar um SaaS multitenant para um projeto que tinha um desafio central: ",[15,16,17],"strong",{},"permitir que várias empresas utilizassem o mesmo sistema, mas mantendo vitrines, dados e identidades visuais separadas",". Antes de pensar nas telas ou nos componentes, a primeira decisão importante foi entender como a aplicação identificaria qual empresa estava sendo acessada e como esse contexto seria compartilhado entre rotas, layout e interface.",[11,20,21,22,25,26,29,30,33,34,37,38,41,42,45,46,49,50,53],{},"Para construir a base do MVP, utilizei ",[15,23,24],{},"Nuxt 3"," no frontend, ",[15,27,28],{},"Nuxt UI 4"," na construção de componentes, ",[15,31,32],{},"Nuxt SEO"," para apoiar a otimização das páginas, ",[15,35,36],{},"Supabase"," como backend principal e ",[15,39,40],{},"Vercel"," para o deploy. Com essa estrutura, a arquitetura foi organizada para resolver três pontos principais: ",[15,43,44],{},"descobrir o tenant a partir da URL",", ",[15,47,48],{},"manter os dados da empresa ativa disponíveis na aplicação"," e ",[15,51,52],{},"refletir sua identidade visual de forma dinâmica na vitrine pública",".",[11,55,56],{},"Para isso, a arquitetura foi organizada com:",[58,59,60,67,77,82,87,92,97],"ul",{},[61,62,63,66],"li",{},[15,64,65],{},"um único banco compartilhado",";",[61,68,69,66],{},[15,70,71,72,76],{},"uma coluna ",[73,74,75],"code",{},"empresa_id"," para relacionar os dados ao tenant",[61,78,79,66],{},[15,80,81],{},"RLS no Supabase para isolamento dos dados",[61,83,84,66],{},[15,85,86],{},"rotas dinâmicas por slug no Nuxt",[61,88,89,66],{},[15,90,91],{},"middleware global para resolver a empresa acessada",[61,93,94,66],{},[15,95,96],{},"Pinia para manter a empresa ativa em memória",[61,98,99,53],{},[15,100,101],{},"CSS variables para aplicar as cores de cada empresa no layout",[11,103,104,105,108],{},"A lógica central é simples: quando o usuário acessa uma URL como ",[73,106,107],{},"\u002Flari-loja",", o sistema precisa transformar esse slug em uma empresa real e disponibilizar essa empresa para o restante da aplicação.",[110,111],"hr",{},[113,114,116],"h2",{"id":115},"a-estratégia-multitenant","A estratégia multitenant",[11,118,119],{},"Existem várias formas de estruturar um SaaS multitenant. Uma delas seria criar um banco separado para cada cliente. Para este projeto, essa opção traria complexidade cedo demais.",[11,121,122,123,126],{},"Por isso, optei por uma arquitetura baseada em ",[15,124,125],{},"row-level multitenancy",". Isso significa que os dados das empresas vivem nas mesmas tabelas, mas cada registro possui uma relação com a empresa dona daquele dado.",[11,128,129],{},"O centro dessa estratégia é o campo:",[131,132,137],"pre",{"className":133,"code":134,"language":135,"meta":136,"style":136},"language-txt shiki shiki-themes material-theme-lighter github-dark github-dark","empresa_id\n","txt","",[73,138,139],{"__ignoreMap":136},[140,141,144],"span",{"class":142,"line":143},"line",1,[140,145,134],{},[11,147,148],{},"Esse campo aparece nas tabelas que pertencem a uma empresa e permite que o banco saiba a qual tenant cada informação pertence.",[11,150,151],{},"Com essa decisão, a aplicação não precisa trocar de banco quando muda de empresa. Ela muda apenas o contexto ativo.",[110,153],{},[113,155,157],{"id":156},"como-o-banco-foi-estruturado","Como o banco foi estruturado",[11,159,160],{},"A base da arquitetura começa com três ideias principais no banco:",[162,163,164,177],"table",{},[165,166,167],"thead",{},[168,169,170,174],"tr",{},[171,172,173],"th",{},"Tabela",[171,175,176],{},"Papel na arquitetura",[178,179,180,191,201],"tbody",{},[168,181,182,188],{},[183,184,185],"td",{},[73,186,187],{},"empresas",[183,189,190],{},"Guarda os dados públicos do tenant, como nome, slug, logo e cores",[168,192,193,198],{},[183,194,195],{},[73,196,197],{},"profiles",[183,199,200],{},"Relaciona o usuário autenticado a uma empresa",[168,202,203,208],{},[183,204,205,206],{},"tabelas com ",[73,207,75],{},[183,209,210],{},"Guardam dados pertencentes a uma empresa específica",[11,212,213,214,216],{},"A tabela ",[73,215,187],{}," funciona como a origem do tenant público.",[11,218,219],{},"Ela precisa ter, no mínimo, algo como:",[131,221,223],{"className":133,"code":222,"language":135,"meta":136,"style":136},"empresas\n- id\n- nome\n- slug\n- logo_url\n- descricao\n- colors\n",[73,224,225,230,236,242,248,254,260],{"__ignoreMap":136},[140,226,227],{"class":142,"line":143},[140,228,229],{},"empresas\n",[140,231,233],{"class":142,"line":232},2,[140,234,235],{},"- id\n",[140,237,239],{"class":142,"line":238},3,[140,240,241],{},"- nome\n",[140,243,245],{"class":142,"line":244},4,[140,246,247],{},"- slug\n",[140,249,251],{"class":142,"line":250},5,[140,252,253],{},"- logo_url\n",[140,255,257],{"class":142,"line":256},6,[140,258,259],{},"- descricao\n",[140,261,263],{"class":142,"line":262},7,[140,264,265],{},"- colors\n",[11,267,268,269,272],{},"O campo ",[73,270,271],{},"slug"," é usado na URL pública, como será explicado posteriormente.",[11,274,268,275,278],{},[73,276,277],{},"colors"," guarda a identidade visual da empresa:",[131,280,284],{"className":281,"code":282,"language":283,"meta":136,"style":136},"language-json shiki shiki-themes material-theme-lighter github-dark github-dark","{\n  \"primary\": \"#2563ab\",\n  \"secondary\": \"#f97316\"\n}\n","json",[73,285,286,292,321,340],{"__ignoreMap":136},[140,287,288],{"class":142,"line":143},[140,289,291],{"class":290},"sG-J9","{\n",[140,293,294,298,302,305,308,312,316,318],{"class":142,"line":232},[140,295,297],{"class":296},"swu5b","  \"",[140,299,301],{"class":300},"sod2m","primary",[140,303,304],{"class":296},"\"",[140,306,307],{"class":290},":",[140,309,311],{"class":310},"sF_wb"," \"",[140,313,315],{"class":314},"s0vBq","#2563ab",[140,317,304],{"class":310},[140,319,320],{"class":290},",\n",[140,322,323,325,328,330,332,334,337],{"class":142,"line":238},[140,324,297],{"class":296},[140,326,327],{"class":300},"secondary",[140,329,304],{"class":296},[140,331,307],{"class":290},[140,333,311],{"class":310},[140,335,336],{"class":314},"#f97316",[140,338,339],{"class":310},"\"\n",[140,341,342],{"class":142,"line":244},[140,343,344],{"class":290},"}\n",[11,346,213,347,349],{},[73,348,197],{}," conecta um usuário autenticado a uma empresa:",[131,351,353],{"className":133,"code":352,"language":135,"meta":136,"style":136},"profiles\n- id\n- empresa_id\n",[73,354,355,360,364],{"__ignoreMap":136},[140,356,357],{"class":142,"line":143},[140,358,359],{},"profiles\n",[140,361,362],{"class":142,"line":232},[140,363,235],{},[140,365,366],{"class":142,"line":238},[140,367,368],{},"- empresa_id\n",[11,370,371],{},"Já as tabelas que possuem dados de uma empresa seguem a mesma lógica:",[131,373,375],{"className":133,"code":374,"language":135,"meta":136,"style":136},"itens\n- id\n- empresa_id\n- nome\n- ...\n",[73,376,377,382,386,390,394],{"__ignoreMap":136},[140,378,379],{"class":142,"line":143},[140,380,381],{},"itens\n",[140,383,384],{"class":142,"line":232},[140,385,235],{},[140,387,388],{"class":142,"line":238},[140,389,368],{},[140,391,392],{"class":142,"line":244},[140,393,241],{},[140,395,396],{"class":142,"line":250},[140,397,398],{},"- ...\n",[11,400,401,402],{},"O ponto importante não é listar todos os campos. O ponto é a decisão: ",[15,403,404,405,53],{},"todo dado que pertence a uma empresa precisa carregar ",[73,406,75],{},[110,408],{},[113,410,412],{"id":411},"como-o-banco-descobre-a-empresa-do-usuário","Como o banco descobre a empresa do usuário",[11,414,415,416,53],{},"Para a parte autenticada do sistema, foi criada a função ",[73,417,418],{},"get_minha_empresa_id()",[11,420,421],{},"Ela permite que o próprio banco descubra qual empresa pertence ao usuário logado.",[131,423,427],{"className":424,"code":425,"language":426,"meta":136,"style":136},"language-sql shiki shiki-themes material-theme-lighter github-dark github-dark","create or replace function get_minha_empresa_id()\nreturns uuid\nlanguage sql\nstable\nsecurity definer\nas $$\n  select empresa_id\n  from profiles\n  where id = auth.uid();\n$$;\n","sql",[73,428,429,434,439,444,449,454,459,464,470,476],{"__ignoreMap":136},[140,430,431],{"class":142,"line":143},[140,432,433],{},"create or replace function get_minha_empresa_id()\n",[140,435,436],{"class":142,"line":232},[140,437,438],{},"returns uuid\n",[140,440,441],{"class":142,"line":238},[140,442,443],{},"language sql\n",[140,445,446],{"class":142,"line":244},[140,447,448],{},"stable\n",[140,450,451],{"class":142,"line":250},[140,452,453],{},"security definer\n",[140,455,456],{"class":142,"line":256},[140,457,458],{},"as $$\n",[140,460,461],{"class":142,"line":262},[140,462,463],{},"  select empresa_id\n",[140,465,467],{"class":142,"line":466},8,[140,468,469],{},"  from profiles\n",[140,471,473],{"class":142,"line":472},9,[140,474,475],{},"  where id = auth.uid();\n",[140,477,479],{"class":142,"line":478},10,[140,480,481],{},"$$;\n",[11,483,484,485,488],{},"O ",[73,486,487],{},"auth.uid()"," vem do Supabase Auth e representa o usuário autenticado na requisição atual.",[11,490,491,492,494,495,53],{},"A função usa esse ID para buscar o ",[73,493,75],{}," correspondente na tabela ",[73,496,197],{},[11,498,499,500,502],{},"Com isso, as policies do RLS não precisam confiar em um ",[73,501,75],{}," enviado pelo frontend. O banco consulta o contexto da sessão e valida o acesso por conta própria.",[11,504,505],{},"Um exemplo reduzido de policy seria:",[131,507,509],{"className":424,"code":508,"language":426,"meta":136,"style":136},"using (empresa_id = get_minha_empresa_id())\nwith check (empresa_id = get_minha_empresa_id())\n",[73,510,511,516],{"__ignoreMap":136},[140,512,513],{"class":142,"line":143},[140,514,515],{},"using (empresa_id = get_minha_empresa_id())\n",[140,517,518],{"class":142,"line":232},[140,519,520],{},"with check (empresa_id = get_minha_empresa_id())\n",[11,522,484,523,526],{},[73,524,525],{},"using"," filtra o que o usuário pode acessar.",[11,528,484,529,532,533,535],{},[73,530,531],{},"with check"," impede que ele crie ou altere registros usando o ",[73,534,75],{}," de outra empresa.",[11,537,538,539],{},"Essa foi uma decisão central da arquitetura: ",[15,540,541],{},"o isolamento dos dados não fica dependente da interface. Essa responsabilidade fica no banco.",[110,543],{},[113,545,547],{"id":546},"o-roteamento-por-slug-nas-páginas-públicas","O roteamento por slug nas páginas públicas",[11,549,550,551,553],{},"Nas páginas públicas, a empresa ativa é identificada pela URL através de um ",[15,552,271],{},". Esse slug funciona como a chave inicial para localizar o tenant no banco, carregar seus dados e disponibilizar esse contexto para o restante da aplicação.",[11,555,556],{},"A estrutura pensada foi:",[131,558,560],{"className":133,"code":559,"language":135,"meta":136,"style":136},"pages\u002F\n├── index.vue\n├── 404.vue\n└── [slug]\u002F\n    └── index.vue\n",[73,561,562,567,572,577,582],{"__ignoreMap":136},[140,563,564],{"class":142,"line":143},[140,565,566],{},"pages\u002F\n",[140,568,569],{"class":142,"line":232},[140,570,571],{},"├── index.vue\n",[140,573,574],{"class":142,"line":238},[140,575,576],{},"├── 404.vue\n",[140,578,579],{"class":142,"line":244},[140,580,581],{},"└── [slug]\u002F\n",[140,583,584],{"class":142,"line":250},[140,585,586],{},"    └── index.vue\n",[11,588,589,590,593],{},"A rota ",[73,591,592],{},"[slug]"," representa a vitrine pública de uma empresa.",[11,595,596],{},"Exemplo:",[131,598,600],{"className":133,"code":599,"language":135,"meta":136,"style":136},"link-da-plataforma.com\u002Flari-loja\n",[73,601,602],{"__ignoreMap":136},[140,603,604],{"class":142,"line":143},[140,605,599],{},[11,607,608],{},"Nesse caso, o Nuxt entende que:",[131,610,614],{"className":611,"code":612,"language":613,"meta":136,"style":136},"language-ts shiki shiki-themes material-theme-lighter github-dark github-dark","slug = 'lari-loja'\n","ts",[73,615,616],{"__ignoreMap":136},[140,617,618,622,626,629,632],{"class":142,"line":143},[140,619,621],{"class":620},"sMo7A","slug ",[140,623,625],{"class":624},"sFfmW","=",[140,627,628],{"class":310}," '",[140,630,631],{"class":314},"lari-loja",[140,633,634],{"class":310},"'\n",[11,636,637],{},"Mas o slug sozinho não resolve o problema. Ele é apenas texto na URL.",[11,639,640],{},"Ainda é necessário descobrir se existe uma empresa cadastrada com esse slug e carregar seus dados.",[11,642,643,644,53],{},"Essa responsabilidade ficou no ",[15,645,646],{},"middleware",[110,648],{},[113,650,652],{"id":651},"middleware-global-transformando-slug-em-empresa","Middleware global: transformando slug em empresa",[11,654,655],{},"O middleware global conecta a URL ao tenant.",[11,657,658,659,661],{},"Ele roda antes da página renderizar, lê o ",[73,660,271],{},", consulta o Supabase e salva a empresa encontrada na store.",[11,663,664],{},"Para que tudo funcione, existe uma função no banco responsável por resolver a empresa a partir do slug recebido pela URL.",[131,666,668],{"className":424,"code":667,"language":426,"meta":136,"style":136},"-- resolver_empresa_por_slug(p_slug text)\ndeclare\n  v_empresa json;\nbegin\n  select json_build_object(\n    'id',            e.id,\n    'slug',          e.slug,\n    'nome',          e.nome,\n    'descricao',     e.descricao,\n    [...]\n    'colors',        e.colors\n  )\n  into v_empresa\n  from empresas e\n  where e.slug = lower(trim(p_slug))\n    and e.is_ativo = true;\n\n  return v_empresa;\nend;\n",[73,669,670,675,680,685,690,695,700,705,710,715,720,726,732,738,744,750,756,763,769],{"__ignoreMap":136},[140,671,672],{"class":142,"line":143},[140,673,674],{},"-- resolver_empresa_por_slug(p_slug text)\n",[140,676,677],{"class":142,"line":232},[140,678,679],{},"declare\n",[140,681,682],{"class":142,"line":238},[140,683,684],{},"  v_empresa json;\n",[140,686,687],{"class":142,"line":244},[140,688,689],{},"begin\n",[140,691,692],{"class":142,"line":250},[140,693,694],{},"  select json_build_object(\n",[140,696,697],{"class":142,"line":256},[140,698,699],{},"    'id',            e.id,\n",[140,701,702],{"class":142,"line":262},[140,703,704],{},"    'slug',          e.slug,\n",[140,706,707],{"class":142,"line":466},[140,708,709],{},"    'nome',          e.nome,\n",[140,711,712],{"class":142,"line":472},[140,713,714],{},"    'descricao',     e.descricao,\n",[140,716,717],{"class":142,"line":478},[140,718,719],{},"    [...]\n",[140,721,723],{"class":142,"line":722},11,[140,724,725],{},"    'colors',        e.colors\n",[140,727,729],{"class":142,"line":728},12,[140,730,731],{},"  )\n",[140,733,735],{"class":142,"line":734},13,[140,736,737],{},"  into v_empresa\n",[140,739,741],{"class":142,"line":740},14,[140,742,743],{},"  from empresas e\n",[140,745,747],{"class":142,"line":746},15,[140,748,749],{},"  where e.slug = lower(trim(p_slug))\n",[140,751,753],{"class":142,"line":752},16,[140,754,755],{},"    and e.is_ativo = true;\n",[140,757,759],{"class":142,"line":758},17,[140,760,762],{"emptyLinePlaceholder":761},true,"\n",[140,764,766],{"class":142,"line":765},18,[140,767,768],{},"  return v_empresa;\n",[140,770,772],{"class":142,"line":771},19,[140,773,774],{},"end;\n",[11,776,777,778,781],{},"Essa função recebe o slug, normaliza o valor com ",[73,779,780],{},"lower(trim(p_slug))"," e busca apenas empresas ativas. O retorno é um JSON com os dados públicos que a vitrine precisa consumir, como nome, descrição, logo e cores.",[11,783,784,785,787],{},"Com isso, o middleware não precisa conhecer a estrutura completa da tabela ",[73,786,187],{},". Ele apenas envia o slug para a RPC e recebe uma versão pública da empresa.",[131,789,791],{"className":611,"code":790,"language":613,"meta":136,"style":136},"\u002F\u002F middleware\u002Ftenant.global.ts\nconst rotasReservadas = ['admin', 'login', 'cadastro', 'checkout', '404'] \u002F\u002F Lista de rotas reservadas\n\nexport default defineNuxtRouteMiddleware(async to => {\n\n  const slugParam = to.params.slug \u002F\u002F Captura o parâmetro slug da URL atual\n\n  if (!slugParam || Array.isArray(slugParam)) return \u002F\u002F Valida o slug e interrompe middleware\n\n  const slug = slugParam \u002F\u002F Atribiu o slug como uma string simples\n\n  if (rotasReservadas.includes(slug)) return \u002F\u002F Se o slug for uma rota reservada, não tenta buscar empresa\n\n  const empresaStore = useEmpresaStore() \u002F\u002F Acessa a store responsável por guardar a empresa ativa\n\n  \u002F\u002F Se a empresa atual já estiver carregada para esse mesmo slug, evita uma nova consulta ao banco\n  if (empresaStore.empresa?.slug === slug) return\n\n  \u002F\u002F Cria o cliente do Supabase para realizar a chamada ao backend\n  const supabase = useSupabaseClient()\n\n  \u002F\u002F Chama a função RPC no Supabase para buscar a empresa correspondente ao slug da URL\n  const { data, error } = await supabase.rpc('resolver_empresa_por_slug', {\n    p_slug: slug\n  })\n\n  \u002F\u002F Se ocorrer erro ou nenhuma empresa for encontrada, redireciona para a página 404\n  if (error || !data) {\n    return navigateTo('\u002F404')\n  }\n\n  \u002F\u002F Salva os dados da empresa encontrada na store\n    empresaStore.definir(data as EmpresaPublica)\n})\n\n",[73,792,793,799,866,870,899,903,927,931,970,974,988,992,1017,1021,1039,1043,1048,1078,1082,1087,1103,1108,1114,1158,1169,1178,1183,1189,1211,1231,1237,1242,1248,1272],{"__ignoreMap":136},[140,794,795],{"class":142,"line":143},[140,796,798],{"class":797},"sutJx","\u002F\u002F middleware\u002Ftenant.global.ts\n",[140,800,801,805,809,812,815,818,821,823,826,828,831,833,835,837,840,842,844,846,849,851,853,855,858,860,863],{"class":142,"line":232},[140,802,804],{"class":803},"sFsEu","const",[140,806,808],{"class":807},"sVPC0"," rotasReservadas",[140,810,811],{"class":624}," =",[140,813,814],{"class":620}," [",[140,816,817],{"class":310},"'",[140,819,820],{"class":314},"admin",[140,822,817],{"class":310},[140,824,825],{"class":290},",",[140,827,628],{"class":310},[140,829,830],{"class":314},"login",[140,832,817],{"class":310},[140,834,825],{"class":290},[140,836,628],{"class":310},[140,838,839],{"class":314},"cadastro",[140,841,817],{"class":310},[140,843,825],{"class":290},[140,845,628],{"class":310},[140,847,848],{"class":314},"checkout",[140,850,817],{"class":310},[140,852,825],{"class":290},[140,854,628],{"class":310},[140,856,857],{"class":314},"404",[140,859,817],{"class":310},[140,861,862],{"class":620},"] ",[140,864,865],{"class":797},"\u002F\u002F Lista de rotas reservadas\n",[140,867,868],{"class":142,"line":238},[140,869,762],{"emptyLinePlaceholder":761},[140,871,872,876,879,883,886,889,893,896],{"class":142,"line":244},[140,873,875],{"class":874},"s3Er8","export",[140,877,878],{"class":874}," default",[140,880,882],{"class":881},"sK_r7"," defineNuxtRouteMiddleware",[140,884,885],{"class":620},"(",[140,887,888],{"class":803},"async",[140,890,892],{"class":891},"sk1zL"," to",[140,894,895],{"class":803}," =>",[140,897,898],{"class":290}," {\n",[140,900,901],{"class":142,"line":250},[140,902,762],{"emptyLinePlaceholder":761},[140,904,905,908,911,913,915,917,920,922,924],{"class":142,"line":256},[140,906,907],{"class":803},"  const",[140,909,910],{"class":807}," slugParam",[140,912,811],{"class":624},[140,914,892],{"class":620},[140,916,53],{"class":290},[140,918,919],{"class":620},"params",[140,921,53],{"class":290},[140,923,271],{"class":620},[140,925,926],{"class":797}," \u002F\u002F Captura o parâmetro slug da URL atual\n",[140,928,929],{"class":142,"line":262},[140,930,762],{"emptyLinePlaceholder":761},[140,932,933,936,940,943,946,949,952,954,957,959,961,964,967],{"class":142,"line":466},[140,934,935],{"class":874},"  if",[140,937,939],{"class":938},"sdv8B"," (",[140,941,942],{"class":624},"!",[140,944,945],{"class":620},"slugParam",[140,947,948],{"class":624}," ||",[140,950,951],{"class":620}," Array",[140,953,53],{"class":290},[140,955,956],{"class":881},"isArray",[140,958,885],{"class":938},[140,960,945],{"class":620},[140,962,963],{"class":938},")) ",[140,965,966],{"class":874},"return",[140,968,969],{"class":797}," \u002F\u002F Valida o slug e interrompe middleware\n",[140,971,972],{"class":142,"line":472},[140,973,762],{"emptyLinePlaceholder":761},[140,975,976,978,981,983,985],{"class":142,"line":478},[140,977,907],{"class":803},[140,979,980],{"class":807}," slug",[140,982,811],{"class":624},[140,984,910],{"class":620},[140,986,987],{"class":797}," \u002F\u002F Atribiu o slug como uma string simples\n",[140,989,990],{"class":142,"line":722},[140,991,762],{"emptyLinePlaceholder":761},[140,993,994,996,998,1001,1003,1006,1008,1010,1012,1014],{"class":142,"line":728},[140,995,935],{"class":874},[140,997,939],{"class":938},[140,999,1000],{"class":620},"rotasReservadas",[140,1002,53],{"class":290},[140,1004,1005],{"class":881},"includes",[140,1007,885],{"class":938},[140,1009,271],{"class":620},[140,1011,963],{"class":938},[140,1013,966],{"class":874},[140,1015,1016],{"class":797}," \u002F\u002F Se o slug for uma rota reservada, não tenta buscar empresa\n",[140,1018,1019],{"class":142,"line":734},[140,1020,762],{"emptyLinePlaceholder":761},[140,1022,1023,1025,1028,1030,1033,1036],{"class":142,"line":740},[140,1024,907],{"class":803},[140,1026,1027],{"class":807}," empresaStore",[140,1029,811],{"class":624},[140,1031,1032],{"class":881}," useEmpresaStore",[140,1034,1035],{"class":938},"() ",[140,1037,1038],{"class":797},"\u002F\u002F Acessa a store responsável por guardar a empresa ativa\n",[140,1040,1041],{"class":142,"line":746},[140,1042,762],{"emptyLinePlaceholder":761},[140,1044,1045],{"class":142,"line":752},[140,1046,1047],{"class":797},"  \u002F\u002F Se a empresa atual já estiver carregada para esse mesmo slug, evita uma nova consulta ao banco\n",[140,1049,1050,1052,1054,1057,1059,1062,1065,1067,1070,1072,1075],{"class":142,"line":758},[140,1051,935],{"class":874},[140,1053,939],{"class":938},[140,1055,1056],{"class":620},"empresaStore",[140,1058,53],{"class":290},[140,1060,1061],{"class":620},"empresa",[140,1063,1064],{"class":290},"?.",[140,1066,271],{"class":620},[140,1068,1069],{"class":624}," ===",[140,1071,980],{"class":620},[140,1073,1074],{"class":938},") ",[140,1076,1077],{"class":874},"return\n",[140,1079,1080],{"class":142,"line":765},[140,1081,762],{"emptyLinePlaceholder":761},[140,1083,1084],{"class":142,"line":771},[140,1085,1086],{"class":797},"  \u002F\u002F Cria o cliente do Supabase para realizar a chamada ao backend\n",[140,1088,1090,1092,1095,1097,1100],{"class":142,"line":1089},20,[140,1091,907],{"class":803},[140,1093,1094],{"class":807}," supabase",[140,1096,811],{"class":624},[140,1098,1099],{"class":881}," useSupabaseClient",[140,1101,1102],{"class":938},"()\n",[140,1104,1106],{"class":142,"line":1105},21,[140,1107,762],{"emptyLinePlaceholder":761},[140,1109,1111],{"class":142,"line":1110},22,[140,1112,1113],{"class":797},"  \u002F\u002F Chama a função RPC no Supabase para buscar a empresa correspondente ao slug da URL\n",[140,1115,1117,1119,1122,1125,1127,1130,1133,1135,1138,1140,1142,1145,1147,1149,1152,1154,1156],{"class":142,"line":1116},23,[140,1118,907],{"class":803},[140,1120,1121],{"class":290}," {",[140,1123,1124],{"class":807}," data",[140,1126,825],{"class":290},[140,1128,1129],{"class":807}," error",[140,1131,1132],{"class":290}," }",[140,1134,811],{"class":624},[140,1136,1137],{"class":874}," await",[140,1139,1094],{"class":620},[140,1141,53],{"class":290},[140,1143,1144],{"class":881},"rpc",[140,1146,885],{"class":938},[140,1148,817],{"class":310},[140,1150,1151],{"class":314},"resolver_empresa_por_slug",[140,1153,817],{"class":310},[140,1155,825],{"class":290},[140,1157,898],{"class":290},[140,1159,1161,1164,1166],{"class":142,"line":1160},24,[140,1162,1163],{"class":938},"    p_slug",[140,1165,307],{"class":290},[140,1167,1168],{"class":620}," slug\n",[140,1170,1172,1175],{"class":142,"line":1171},25,[140,1173,1174],{"class":290},"  }",[140,1176,1177],{"class":938},")\n",[140,1179,1181],{"class":142,"line":1180},26,[140,1182,762],{"emptyLinePlaceholder":761},[140,1184,1186],{"class":142,"line":1185},27,[140,1187,1188],{"class":797},"  \u002F\u002F Se ocorrer erro ou nenhuma empresa for encontrada, redireciona para a página 404\n",[140,1190,1192,1194,1196,1199,1201,1204,1207,1209],{"class":142,"line":1191},28,[140,1193,935],{"class":874},[140,1195,939],{"class":938},[140,1197,1198],{"class":620},"error",[140,1200,948],{"class":624},[140,1202,1203],{"class":624}," !",[140,1205,1206],{"class":620},"data",[140,1208,1074],{"class":938},[140,1210,291],{"class":290},[140,1212,1214,1217,1220,1222,1224,1227,1229],{"class":142,"line":1213},29,[140,1215,1216],{"class":874},"    return",[140,1218,1219],{"class":881}," navigateTo",[140,1221,885],{"class":938},[140,1223,817],{"class":310},[140,1225,1226],{"class":314},"\u002F404",[140,1228,817],{"class":310},[140,1230,1177],{"class":938},[140,1232,1234],{"class":142,"line":1233},30,[140,1235,1236],{"class":290},"  }\n",[140,1238,1240],{"class":142,"line":1239},31,[140,1241,762],{"emptyLinePlaceholder":761},[140,1243,1245],{"class":142,"line":1244},32,[140,1246,1247],{"class":797},"  \u002F\u002F Salva os dados da empresa encontrada na store\n",[140,1249,1251,1254,1256,1259,1261,1263,1266,1270],{"class":142,"line":1250},33,[140,1252,1253],{"class":620},"    empresaStore",[140,1255,53],{"class":290},[140,1257,1258],{"class":881},"definir",[140,1260,885],{"class":938},[140,1262,1206],{"class":620},[140,1264,1265],{"class":874}," as",[140,1267,1269],{"class":1268},"soiBB"," EmpresaPublica",[140,1271,1177],{"class":938},[140,1273,1275,1278],{"class":142,"line":1274},34,[140,1276,1277],{"class":290},"}",[140,1279,1177],{"class":620},[11,1281,1282,1283,53],{},"Esse middleware tem uma responsabilidade bem específica: ",[15,1284,1285],{},"resolver o tenant da rota atual",[58,1287,1288,1291,1294,1297],{},[61,1289,1290],{},"Ele não aplica tema;",[61,1292,1293],{},"Ele não renderiza página;",[61,1295,1296],{},"Ele não monta catálogo;",[61,1298,1299,1300,1302],{},"Ele apenas pega o slug, consulta a função ",[73,1301,1151],{}," e salva o resultado na store;",[11,1304,1305,1306,1309],{},"Essa separação deixa a rota ",[73,1307,1308],{},"[slug]\u002Findex.vue"," mais simples, porque ela não precisa conhecer os detalhes de como a empresa foi encontrada.",[110,1311],{},[113,1313,1315],{"id":1314},"salvando-a-empresa-ativa-na-store","Salvando a empresa ativa na store",[11,1317,1318],{},"Depois que o middleware encontra a empresa, os dados precisam ficar disponíveis para o restante da aplicação.",[11,1320,1321],{},"Para isso, foi criada uma store com Pinia.",[131,1323,1325],{"className":611,"code":1324,"language":613,"meta":136,"style":136},"\u002F\u002F stores\u002FuseEmpresaStore.ts\nexport const useEmpresaStore = defineStore('empresa', () => {\n  const empresa = ref\u003CEmpresaPublica | null>(null)\n\n  const nomeEmpresa = computed(() => empresa.value?.nome ?? '')\n\n  function definir(dados: EmpresaPublica) {\n    empresa.value = dados\n  }\n\n  function limpar() {\n    empresa.value = null\n  }\n\n  return {\n    empresa,\n    nomeEmpresa,\n    definir,\n    limpar\n  }\n})\n",[73,1326,1327,1332,1363,1398,1402,1441,1445,1467,1481,1485,1489,1500,1513,1517,1521,1528,1534,1541,1548,1553,1557],{"__ignoreMap":136},[140,1328,1329],{"class":142,"line":143},[140,1330,1331],{"class":797},"\u002F\u002F stores\u002FuseEmpresaStore.ts\n",[140,1333,1334,1336,1339,1341,1343,1346,1348,1350,1352,1354,1356,1359,1361],{"class":142,"line":232},[140,1335,875],{"class":874},[140,1337,1338],{"class":803}," const",[140,1340,1032],{"class":807},[140,1342,811],{"class":624},[140,1344,1345],{"class":881}," defineStore",[140,1347,885],{"class":620},[140,1349,817],{"class":310},[140,1351,1061],{"class":314},[140,1353,817],{"class":310},[140,1355,825],{"class":290},[140,1357,1358],{"class":290}," ()",[140,1360,895],{"class":803},[140,1362,898],{"class":290},[140,1364,1365,1367,1370,1372,1375,1378,1381,1384,1388,1391,1393,1396],{"class":142,"line":238},[140,1366,907],{"class":803},[140,1368,1369],{"class":807}," empresa",[140,1371,811],{"class":624},[140,1373,1374],{"class":881}," ref",[140,1376,1377],{"class":290},"\u003C",[140,1379,1380],{"class":1268},"EmpresaPublica",[140,1382,1383],{"class":624}," |",[140,1385,1387],{"class":1386},"s3afY"," null",[140,1389,1390],{"class":290},">",[140,1392,885],{"class":938},[140,1394,1395],{"class":296},"null",[140,1397,1177],{"class":938},[140,1399,1400],{"class":142,"line":244},[140,1401,762],{"emptyLinePlaceholder":761},[140,1403,1404,1406,1409,1411,1414,1416,1419,1421,1423,1425,1428,1430,1433,1436,1439],{"class":142,"line":250},[140,1405,907],{"class":803},[140,1407,1408],{"class":807}," nomeEmpresa",[140,1410,811],{"class":624},[140,1412,1413],{"class":881}," computed",[140,1415,885],{"class":938},[140,1417,1418],{"class":290},"()",[140,1420,895],{"class":803},[140,1422,1369],{"class":620},[140,1424,53],{"class":290},[140,1426,1427],{"class":620},"value",[140,1429,1064],{"class":290},[140,1431,1432],{"class":620},"nome",[140,1434,1435],{"class":624}," ??",[140,1437,1438],{"class":310}," ''",[140,1440,1177],{"class":938},[140,1442,1443],{"class":142,"line":256},[140,1444,762],{"emptyLinePlaceholder":761},[140,1446,1447,1450,1453,1455,1458,1460,1462,1465],{"class":142,"line":262},[140,1448,1449],{"class":803},"  function",[140,1451,1452],{"class":881}," definir",[140,1454,885],{"class":290},[140,1456,1457],{"class":891},"dados",[140,1459,307],{"class":624},[140,1461,1269],{"class":1268},[140,1463,1464],{"class":290},")",[140,1466,898],{"class":290},[140,1468,1469,1472,1474,1476,1478],{"class":142,"line":466},[140,1470,1471],{"class":620},"    empresa",[140,1473,53],{"class":290},[140,1475,1427],{"class":620},[140,1477,811],{"class":624},[140,1479,1480],{"class":620}," dados\n",[140,1482,1483],{"class":142,"line":472},[140,1484,1236],{"class":290},[140,1486,1487],{"class":142,"line":478},[140,1488,762],{"emptyLinePlaceholder":761},[140,1490,1491,1493,1496,1498],{"class":142,"line":722},[140,1492,1449],{"class":803},[140,1494,1495],{"class":881}," limpar",[140,1497,1418],{"class":290},[140,1499,898],{"class":290},[140,1501,1502,1504,1506,1508,1510],{"class":142,"line":728},[140,1503,1471],{"class":620},[140,1505,53],{"class":290},[140,1507,1427],{"class":620},[140,1509,811],{"class":624},[140,1511,1512],{"class":296}," null\n",[140,1514,1515],{"class":142,"line":734},[140,1516,1236],{"class":290},[140,1518,1519],{"class":142,"line":740},[140,1520,762],{"emptyLinePlaceholder":761},[140,1522,1523,1526],{"class":142,"line":746},[140,1524,1525],{"class":874},"  return",[140,1527,898],{"class":290},[140,1529,1530,1532],{"class":142,"line":752},[140,1531,1471],{"class":620},[140,1533,320],{"class":290},[140,1535,1536,1539],{"class":142,"line":758},[140,1537,1538],{"class":620},"    nomeEmpresa",[140,1540,320],{"class":290},[140,1542,1543,1546],{"class":142,"line":765},[140,1544,1545],{"class":620},"    definir",[140,1547,320],{"class":290},[140,1549,1550],{"class":142,"line":771},[140,1551,1552],{"class":620},"    limpar\n",[140,1554,1555],{"class":142,"line":1089},[140,1556,1236],{"class":290},[140,1558,1559,1561],{"class":142,"line":1105},[140,1560,1277],{"class":290},[140,1562,1177],{"class":620},[58,1564,1565,1568,1571],{},[61,1566,1567],{},"A store não busca dados;",[61,1569,1570],{},"Não chama Supabase;",[61,1572,1573],{},"Não conhece a rota;",[11,1575,1576,1577,1580],{},"Ela apenas ",[15,1578,1579],{},"guarda a empresa ativa",". Essa é a vantagem de separar as camadas: o middleware resolve, a store armazena e as páginas consomem.",[110,1582],{},[113,1584,1586,1587],{"id":1585},"consumindo-a-store-na-rota-slugindexvue","Consumindo a store na rota ",[73,1588,1308],{},[11,1590,1591],{},"Com a empresa já salva na store, a página da vitrine consome esse contexto sem repetir a lógica de resolução do slug.",[131,1593,1595],{"className":611,"code":1594,"language":613,"meta":136,"style":136},"\u002F\u002F pages\u002F[slug]\u002Findex.vue\ndefinePageMeta({ layout: 'catalog' })\n\nconst empresaStore = useEmpresaStore() \u002F\u002F Acessa a store da empresa\nconst empresa = computed(() => empresaStore.empresa) \u002F\u002F Cria uma referência reativa para a empresa ativa\n\n\u002F\u002F Define os metadados SEO com base na empresa ativa\nuseSeoMeta({\n  title: computed(() => empresa.value?.nome || ''),\n  description: computed(() => empresa.value?.descricao || ''),\n  ogTitle: computed(() => empresa.value?.nome || ''),\n  ogDescription: computed(() => empresa.value?.descricao || ''),\n  ogImage: computed(() => empresa.value?.logo_url || '')\n})\n\n",[73,1596,1597,1602,1628,1632,1647,1673,1677,1682,1691,1726,1760,1793,1826,1858],{"__ignoreMap":136},[140,1598,1599],{"class":142,"line":143},[140,1600,1601],{"class":797},"\u002F\u002F pages\u002F[slug]\u002Findex.vue\n",[140,1603,1604,1607,1609,1612,1615,1617,1619,1622,1624,1626],{"class":142,"line":232},[140,1605,1606],{"class":881},"definePageMeta",[140,1608,885],{"class":620},[140,1610,1611],{"class":290},"{",[140,1613,1614],{"class":938}," layout",[140,1616,307],{"class":290},[140,1618,628],{"class":310},[140,1620,1621],{"class":314},"catalog",[140,1623,817],{"class":310},[140,1625,1132],{"class":290},[140,1627,1177],{"class":620},[140,1629,1630],{"class":142,"line":238},[140,1631,762],{"emptyLinePlaceholder":761},[140,1633,1634,1636,1638,1640,1642,1644],{"class":142,"line":244},[140,1635,804],{"class":803},[140,1637,1027],{"class":807},[140,1639,811],{"class":624},[140,1641,1032],{"class":881},[140,1643,1035],{"class":620},[140,1645,1646],{"class":797},"\u002F\u002F Acessa a store da empresa\n",[140,1648,1649,1651,1653,1655,1657,1659,1661,1663,1665,1667,1670],{"class":142,"line":250},[140,1650,804],{"class":803},[140,1652,1369],{"class":807},[140,1654,811],{"class":624},[140,1656,1413],{"class":881},[140,1658,885],{"class":620},[140,1660,1418],{"class":290},[140,1662,895],{"class":803},[140,1664,1027],{"class":620},[140,1666,53],{"class":290},[140,1668,1669],{"class":620},"empresa) ",[140,1671,1672],{"class":797},"\u002F\u002F Cria uma referência reativa para a empresa ativa\n",[140,1674,1675],{"class":142,"line":256},[140,1676,762],{"emptyLinePlaceholder":761},[140,1678,1679],{"class":142,"line":262},[140,1680,1681],{"class":797},"\u002F\u002F Define os metadados SEO com base na empresa ativa\n",[140,1683,1684,1687,1689],{"class":142,"line":466},[140,1685,1686],{"class":881},"useSeoMeta",[140,1688,885],{"class":620},[140,1690,291],{"class":290},[140,1692,1693,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714,1717,1720,1722,1724],{"class":142,"line":472},[140,1694,1695],{"class":938},"  title",[140,1697,307],{"class":290},[140,1699,1413],{"class":881},[140,1701,885],{"class":620},[140,1703,1418],{"class":290},[140,1705,895],{"class":803},[140,1707,1369],{"class":620},[140,1709,53],{"class":290},[140,1711,1427],{"class":620},[140,1713,1064],{"class":290},[140,1715,1716],{"class":620},"nome ",[140,1718,1719],{"class":624},"||",[140,1721,1438],{"class":310},[140,1723,1464],{"class":620},[140,1725,320],{"class":290},[140,1727,1728,1731,1733,1735,1737,1739,1741,1743,1745,1747,1749,1752,1754,1756,1758],{"class":142,"line":478},[140,1729,1730],{"class":938},"  description",[140,1732,307],{"class":290},[140,1734,1413],{"class":881},[140,1736,885],{"class":620},[140,1738,1418],{"class":290},[140,1740,895],{"class":803},[140,1742,1369],{"class":620},[140,1744,53],{"class":290},[140,1746,1427],{"class":620},[140,1748,1064],{"class":290},[140,1750,1751],{"class":620},"descricao ",[140,1753,1719],{"class":624},[140,1755,1438],{"class":310},[140,1757,1464],{"class":620},[140,1759,320],{"class":290},[140,1761,1762,1765,1767,1769,1771,1773,1775,1777,1779,1781,1783,1785,1787,1789,1791],{"class":142,"line":722},[140,1763,1764],{"class":938},"  ogTitle",[140,1766,307],{"class":290},[140,1768,1413],{"class":881},[140,1770,885],{"class":620},[140,1772,1418],{"class":290},[140,1774,895],{"class":803},[140,1776,1369],{"class":620},[140,1778,53],{"class":290},[140,1780,1427],{"class":620},[140,1782,1064],{"class":290},[140,1784,1716],{"class":620},[140,1786,1719],{"class":624},[140,1788,1438],{"class":310},[140,1790,1464],{"class":620},[140,1792,320],{"class":290},[140,1794,1795,1798,1800,1802,1804,1806,1808,1810,1812,1814,1816,1818,1820,1822,1824],{"class":142,"line":728},[140,1796,1797],{"class":938},"  ogDescription",[140,1799,307],{"class":290},[140,1801,1413],{"class":881},[140,1803,885],{"class":620},[140,1805,1418],{"class":290},[140,1807,895],{"class":803},[140,1809,1369],{"class":620},[140,1811,53],{"class":290},[140,1813,1427],{"class":620},[140,1815,1064],{"class":290},[140,1817,1751],{"class":620},[140,1819,1719],{"class":624},[140,1821,1438],{"class":310},[140,1823,1464],{"class":620},[140,1825,320],{"class":290},[140,1827,1828,1831,1833,1835,1837,1839,1841,1843,1845,1847,1849,1852,1854,1856],{"class":142,"line":734},[140,1829,1830],{"class":938},"  ogImage",[140,1832,307],{"class":290},[140,1834,1413],{"class":881},[140,1836,885],{"class":620},[140,1838,1418],{"class":290},[140,1840,895],{"class":803},[140,1842,1369],{"class":620},[140,1844,53],{"class":290},[140,1846,1427],{"class":620},[140,1848,1064],{"class":290},[140,1850,1851],{"class":620},"logo_url ",[140,1853,1719],{"class":624},[140,1855,1438],{"class":310},[140,1857,1177],{"class":620},[140,1859,1860,1862],{"class":142,"line":740},[140,1861,1277],{"class":290},[140,1863,1177],{"class":620},[11,1865,1866,1867,49,1870,1873],{},"O papel dessa página é ",[15,1868,1869],{},"consumir o tenant ativo",[15,1871,1872],{},"renderizar a experiência pública"," daquela empresa, sem buscar tudo novamente. Essa responsabilidade pertence ao middleware.",[11,1875,1876],{},"Na page, o uso fica direto:",[131,1878,1882],{"className":1879,"code":1880,"language":1881,"meta":136,"style":136},"language-vue shiki shiki-themes material-theme-lighter github-dark github-dark","\u003Ctemplate>\n  \u003Csection>\n    \u003Cp class=\"text-sm font-medium text-(--company-primary)\">Vitrine\u003C\u002Fp>\n    \u003Ch1>{{ empresa?.nome }}\u003C\u002Fh1>\n  \u003C\u002Fsection>\n\u003C\u002Ftemplate>\n","vue",[73,1883,1884,1895,1905,1937,1955,1964],{"__ignoreMap":136},[140,1885,1886,1888,1892],{"class":142,"line":143},[140,1887,1377],{"class":290},[140,1889,1891],{"class":1890},"sqIbZ","template",[140,1893,1894],{"class":290},">\n",[140,1896,1897,1900,1903],{"class":142,"line":232},[140,1898,1899],{"class":290},"  \u003C",[140,1901,1902],{"class":1890},"section",[140,1904,1894],{"class":290},[140,1906,1907,1910,1912,1916,1918,1920,1923,1925,1927,1930,1933,1935],{"class":142,"line":238},[140,1908,1909],{"class":290},"    \u003C",[140,1911,11],{"class":1890},[140,1913,1915],{"class":1914},"s7047"," class",[140,1917,625],{"class":290},[140,1919,304],{"class":310},[140,1921,1922],{"class":314},"text-sm font-medium text-(--company-primary)",[140,1924,304],{"class":310},[140,1926,1390],{"class":290},[140,1928,1929],{"class":620},"Vitrine",[140,1931,1932],{"class":290},"\u003C\u002F",[140,1934,11],{"class":1890},[140,1936,1894],{"class":290},[140,1938,1939,1941,1944,1946,1949,1951,1953],{"class":142,"line":244},[140,1940,1909],{"class":290},[140,1942,1943],{"class":1890},"h1",[140,1945,1390],{"class":290},[140,1947,1948],{"class":620},"{{ empresa?.nome }}",[140,1950,1932],{"class":290},[140,1952,1943],{"class":1890},[140,1954,1894],{"class":290},[140,1956,1957,1960,1962],{"class":142,"line":250},[140,1958,1959],{"class":290},"  \u003C\u002F",[140,1961,1902],{"class":1890},[140,1963,1894],{"class":290},[140,1965,1966,1968,1970],{"class":142,"line":256},[140,1967,1932],{"class":290},[140,1969,1891],{"class":1890},[140,1971,1894],{"class":290},[11,1973,1974],{},"A página consome a empresa ativa vinda da store. Ela não precisa saber todo o caminho feito até essa empresa chegar ali.",[110,1976],{},[113,1978,1980],{"id":1979},"identidade-visual-por-empresa","Identidade visual por empresa",[11,1982,1983,1984,1986,1987,53],{},"Além de resolver qual empresa está ativa, a arquitetura também precisava refletir a identidade visual de cada tenant.\nA decisão foi salvar as cores no campo ",[73,1985,277],{}," da tabela ",[73,1988,187],{},[11,1990,596],{},[131,1992,1993],{"className":281,"code":282,"language":283,"meta":136,"style":136},[73,1994,1995,1999,2017,2033],{"__ignoreMap":136},[140,1996,1997],{"class":142,"line":143},[140,1998,291],{"class":290},[140,2000,2001,2003,2005,2007,2009,2011,2013,2015],{"class":142,"line":232},[140,2002,297],{"class":296},[140,2004,301],{"class":300},[140,2006,304],{"class":296},[140,2008,307],{"class":290},[140,2010,311],{"class":310},[140,2012,315],{"class":314},[140,2014,304],{"class":310},[140,2016,320],{"class":290},[140,2018,2019,2021,2023,2025,2027,2029,2031],{"class":142,"line":238},[140,2020,297],{"class":296},[140,2022,327],{"class":300},[140,2024,304],{"class":296},[140,2026,307],{"class":290},[140,2028,311],{"class":310},[140,2030,336],{"class":314},[140,2032,339],{"class":310},[140,2034,2035],{"class":142,"line":244},[140,2036,344],{"class":290},[11,2038,2039],{},"Essas cores são carregadas junto com a empresa no middleware, salvas na store e depois aplicadas no layout da vitrine.",[110,2041],{},[113,2043,2045],{"id":2044},"composable-de-tema-da-empresa","Composable de tema da empresa",[11,2047,2048],{},"Para evitar lógica repetida, foi criado um composable específico para transformar as cores da empresa em CSS variables.",[11,2050,484,2051,2054],{},[73,2052,2053],{},"useEmpresaTheme()"," centraliza a leitura e validação das cores.",[131,2056,2058],{"className":611,"code":2057,"language":613,"meta":136,"style":136},"\u002F\u002F composables\u002FuseEmpresaTheme.ts\nimport type { CSSProperties } from 'vue'\n\n\u002F\u002F Define um tipo para aceitar propriedades CSS comuns\n\u002F\u002F e também variáveis CSS customizadas, como --company-primary\ntype CSSVars = CSSProperties & Record\u003C`--${string}`, string>\n\n\u002F\u002F Valida se a cor recebida está no formato hexadecimal\nfunction isHexColor(color?: string | null) {\n  if (!color) return false\n  return \u002F^#([0-9A-F]{3}){1,2}$\u002Fi.test(color)\n}\n\nexport function useEmpresaTheme() {\n\n  const empresaStore = useEmpresaStore() \u002F\u002F Acessa a store da empresa ativa\n\n  \u002F\u002F Define a cor primária da empresa\n  const primary = computed(() => {\n    const color = empresaStore.empresa?.colors?.primary\n    return isHexColor(color) ? color : '#2563ab'\n  })\n\n  \u002F\u002F Define a cor secundária da empresa\n  const secondary = computed(() => {\n    const color = empresaStore.empresa?.colors?.secondary\n    return isHexColor(color) ? color : '#f97316'\n  })\n\n  \u002F\u002F Monta as variáveis CSS que serão aplicadas no layout\n  const themeStyle = computed\u003CCSSVars>(() => ({\n    '--company-primary': primary.value,\n    '--company-secondary': secondary.value\n  }))\n\n  \u002F\u002F Expõe as cores e o objeto de estilo para uso no layout\n  return {\n    primary,\n    secondary,\n    themeStyle\n  }\n}\n\n",[73,2059,2060,2065,2089,2093,2098,2103,2145,2149,2154,2180,2198,2254,2258,2262,2276,2280,2295,2299,2304,2323,2348,2374,2380,2384,2389,2408,2431,2455,2461,2465,2470,2498,2519,2537,2544,2549,2555,2562,2570,2578,2584,2589],{"__ignoreMap":136},[140,2061,2062],{"class":142,"line":143},[140,2063,2064],{"class":797},"\u002F\u002F composables\u002FuseEmpresaTheme.ts\n",[140,2066,2067,2070,2073,2075,2078,2080,2083,2085,2087],{"class":142,"line":232},[140,2068,2069],{"class":874},"import",[140,2071,2072],{"class":874}," type",[140,2074,1121],{"class":290},[140,2076,2077],{"class":620}," CSSProperties",[140,2079,1132],{"class":290},[140,2081,2082],{"class":874}," from",[140,2084,628],{"class":310},[140,2086,1881],{"class":314},[140,2088,634],{"class":310},[140,2090,2091],{"class":142,"line":238},[140,2092,762],{"emptyLinePlaceholder":761},[140,2094,2095],{"class":142,"line":244},[140,2096,2097],{"class":797},"\u002F\u002F Define um tipo para aceitar propriedades CSS comuns\n",[140,2099,2100],{"class":142,"line":250},[140,2101,2102],{"class":797},"\u002F\u002F e também variáveis CSS customizadas, como --company-primary\n",[140,2104,2105,2108,2111,2113,2115,2118,2121,2123,2126,2129,2132,2135,2138,2140,2143],{"class":142,"line":256},[140,2106,2107],{"class":803},"type",[140,2109,2110],{"class":1268}," CSSVars",[140,2112,811],{"class":624},[140,2114,2077],{"class":1268},[140,2116,2117],{"class":624}," &",[140,2119,2120],{"class":1268}," Record",[140,2122,1377],{"class":290},[140,2124,2125],{"class":310},"`",[140,2127,2128],{"class":314},"--",[140,2130,2131],{"class":310},"${",[140,2133,2134],{"class":1386},"string",[140,2136,2137],{"class":310},"}`",[140,2139,825],{"class":290},[140,2141,2142],{"class":1386}," string",[140,2144,1894],{"class":290},[140,2146,2147],{"class":142,"line":262},[140,2148,762],{"emptyLinePlaceholder":761},[140,2150,2151],{"class":142,"line":466},[140,2152,2153],{"class":797},"\u002F\u002F Valida se a cor recebida está no formato hexadecimal\n",[140,2155,2156,2159,2162,2164,2167,2170,2172,2174,2176,2178],{"class":142,"line":472},[140,2157,2158],{"class":803},"function",[140,2160,2161],{"class":881}," isHexColor",[140,2163,885],{"class":290},[140,2165,2166],{"class":891},"color",[140,2168,2169],{"class":624},"?:",[140,2171,2142],{"class":1386},[140,2173,1383],{"class":624},[140,2175,1387],{"class":1386},[140,2177,1464],{"class":290},[140,2179,898],{"class":290},[140,2181,2182,2184,2186,2188,2190,2192,2194],{"class":142,"line":478},[140,2183,935],{"class":874},[140,2185,939],{"class":938},[140,2187,942],{"class":624},[140,2189,2166],{"class":620},[140,2191,1074],{"class":938},[140,2193,966],{"class":874},[140,2195,2197],{"class":2196},"sMrrN"," false\n",[140,2199,2200,2202,2205,2208,2212,2215,2218,2222,2225,2228,2230,2233,2236,2239,2243,2245,2248,2250,2252],{"class":142,"line":722},[140,2201,1525],{"class":874},[140,2203,2204],{"class":310}," \u002F",[140,2206,2207],{"class":874},"^",[140,2209,2211],{"class":2210},"sm4dI","#",[140,2213,885],{"class":2214},"svU9d",[140,2216,2217],{"class":296},"[",[140,2219,2221],{"class":2220},"sSJ72","0-9A-F",[140,2223,2224],{"class":296},"]",[140,2226,2227],{"class":624},"{3}",[140,2229,1464],{"class":2214},[140,2231,2232],{"class":624},"{1,2}",[140,2234,2235],{"class":874},"$",[140,2237,2238],{"class":310},"\u002F",[140,2240,2242],{"class":2241},"s1Wpa","i",[140,2244,53],{"class":290},[140,2246,2247],{"class":881},"test",[140,2249,885],{"class":938},[140,2251,2166],{"class":620},[140,2253,1177],{"class":938},[140,2255,2256],{"class":142,"line":728},[140,2257,344],{"class":290},[140,2259,2260],{"class":142,"line":734},[140,2261,762],{"emptyLinePlaceholder":761},[140,2263,2264,2266,2269,2272,2274],{"class":142,"line":740},[140,2265,875],{"class":874},[140,2267,2268],{"class":803}," function",[140,2270,2271],{"class":881}," useEmpresaTheme",[140,2273,1418],{"class":290},[140,2275,898],{"class":290},[140,2277,2278],{"class":142,"line":746},[140,2279,762],{"emptyLinePlaceholder":761},[140,2281,2282,2284,2286,2288,2290,2292],{"class":142,"line":752},[140,2283,907],{"class":803},[140,2285,1027],{"class":807},[140,2287,811],{"class":624},[140,2289,1032],{"class":881},[140,2291,1035],{"class":938},[140,2293,2294],{"class":797},"\u002F\u002F Acessa a store da empresa ativa\n",[140,2296,2297],{"class":142,"line":758},[140,2298,762],{"emptyLinePlaceholder":761},[140,2300,2301],{"class":142,"line":765},[140,2302,2303],{"class":797},"  \u002F\u002F Define a cor primária da empresa\n",[140,2305,2306,2308,2311,2313,2315,2317,2319,2321],{"class":142,"line":771},[140,2307,907],{"class":803},[140,2309,2310],{"class":807}," primary",[140,2312,811],{"class":624},[140,2314,1413],{"class":881},[140,2316,885],{"class":938},[140,2318,1418],{"class":290},[140,2320,895],{"class":803},[140,2322,898],{"class":290},[140,2324,2325,2328,2331,2333,2335,2337,2339,2341,2343,2345],{"class":142,"line":1089},[140,2326,2327],{"class":803},"    const",[140,2329,2330],{"class":807}," color",[140,2332,811],{"class":624},[140,2334,1027],{"class":620},[140,2336,53],{"class":290},[140,2338,1061],{"class":620},[140,2340,1064],{"class":290},[140,2342,277],{"class":620},[140,2344,1064],{"class":290},[140,2346,2347],{"class":620},"primary\n",[140,2349,2350,2352,2354,2356,2358,2360,2363,2365,2368,2370,2372],{"class":142,"line":1105},[140,2351,1216],{"class":874},[140,2353,2161],{"class":881},[140,2355,885],{"class":938},[140,2357,2166],{"class":620},[140,2359,1074],{"class":938},[140,2361,2362],{"class":624},"?",[140,2364,2330],{"class":620},[140,2366,2367],{"class":624}," :",[140,2369,628],{"class":310},[140,2371,315],{"class":314},[140,2373,634],{"class":310},[140,2375,2376,2378],{"class":142,"line":1110},[140,2377,1174],{"class":290},[140,2379,1177],{"class":938},[140,2381,2382],{"class":142,"line":1116},[140,2383,762],{"emptyLinePlaceholder":761},[140,2385,2386],{"class":142,"line":1160},[140,2387,2388],{"class":797},"  \u002F\u002F Define a cor secundária da empresa\n",[140,2390,2391,2393,2396,2398,2400,2402,2404,2406],{"class":142,"line":1171},[140,2392,907],{"class":803},[140,2394,2395],{"class":807}," secondary",[140,2397,811],{"class":624},[140,2399,1413],{"class":881},[140,2401,885],{"class":938},[140,2403,1418],{"class":290},[140,2405,895],{"class":803},[140,2407,898],{"class":290},[140,2409,2410,2412,2414,2416,2418,2420,2422,2424,2426,2428],{"class":142,"line":1180},[140,2411,2327],{"class":803},[140,2413,2330],{"class":807},[140,2415,811],{"class":624},[140,2417,1027],{"class":620},[140,2419,53],{"class":290},[140,2421,1061],{"class":620},[140,2423,1064],{"class":290},[140,2425,277],{"class":620},[140,2427,1064],{"class":290},[140,2429,2430],{"class":620},"secondary\n",[140,2432,2433,2435,2437,2439,2441,2443,2445,2447,2449,2451,2453],{"class":142,"line":1185},[140,2434,1216],{"class":874},[140,2436,2161],{"class":881},[140,2438,885],{"class":938},[140,2440,2166],{"class":620},[140,2442,1074],{"class":938},[140,2444,2362],{"class":624},[140,2446,2330],{"class":620},[140,2448,2367],{"class":624},[140,2450,628],{"class":310},[140,2452,336],{"class":314},[140,2454,634],{"class":310},[140,2456,2457,2459],{"class":142,"line":1191},[140,2458,1174],{"class":290},[140,2460,1177],{"class":938},[140,2462,2463],{"class":142,"line":1213},[140,2464,762],{"emptyLinePlaceholder":761},[140,2466,2467],{"class":142,"line":1233},[140,2468,2469],{"class":797},"  \u002F\u002F Monta as variáveis CSS que serão aplicadas no layout\n",[140,2471,2472,2474,2477,2479,2481,2483,2486,2488,2490,2492,2494,2496],{"class":142,"line":1239},[140,2473,907],{"class":803},[140,2475,2476],{"class":807}," themeStyle",[140,2478,811],{"class":624},[140,2480,1413],{"class":881},[140,2482,1377],{"class":290},[140,2484,2485],{"class":1268},"CSSVars",[140,2487,1390],{"class":290},[140,2489,885],{"class":938},[140,2491,1418],{"class":290},[140,2493,895],{"class":803},[140,2495,939],{"class":938},[140,2497,291],{"class":290},[140,2499,2500,2503,2507,2509,2511,2513,2515,2517],{"class":142,"line":1244},[140,2501,2502],{"class":310},"    '",[140,2504,2506],{"class":2505},"swHuM","--company-primary",[140,2508,817],{"class":310},[140,2510,307],{"class":290},[140,2512,2310],{"class":620},[140,2514,53],{"class":290},[140,2516,1427],{"class":620},[140,2518,320],{"class":290},[140,2520,2521,2523,2526,2528,2530,2532,2534],{"class":142,"line":1250},[140,2522,2502],{"class":310},[140,2524,2525],{"class":2505},"--company-secondary",[140,2527,817],{"class":310},[140,2529,307],{"class":290},[140,2531,2395],{"class":620},[140,2533,53],{"class":290},[140,2535,2536],{"class":620},"value\n",[140,2538,2539,2541],{"class":142,"line":1274},[140,2540,1174],{"class":290},[140,2542,2543],{"class":938},"))\n",[140,2545,2547],{"class":142,"line":2546},35,[140,2548,762],{"emptyLinePlaceholder":761},[140,2550,2552],{"class":142,"line":2551},36,[140,2553,2554],{"class":797},"  \u002F\u002F Expõe as cores e o objeto de estilo para uso no layout\n",[140,2556,2558,2560],{"class":142,"line":2557},37,[140,2559,1525],{"class":874},[140,2561,898],{"class":290},[140,2563,2565,2568],{"class":142,"line":2564},38,[140,2566,2567],{"class":620},"    primary",[140,2569,320],{"class":290},[140,2571,2573,2576],{"class":142,"line":2572},39,[140,2574,2575],{"class":620},"    secondary",[140,2577,320],{"class":290},[140,2579,2581],{"class":142,"line":2580},40,[140,2582,2583],{"class":620},"    themeStyle\n",[140,2585,2587],{"class":142,"line":2586},41,[140,2588,1236],{"class":290},[140,2590,2592],{"class":142,"line":2591},42,[140,2593,344],{"class":290},[11,2595,2596],{},"Esse composable evita que a validação de cor fique espalhada em várias páginas. Se a empresa tiver uma cor válida, o sistema usa a cor cadastrada, se não tiver, usa uma cor padrão.\nAssim, a interface nunca fica sem tema.",[110,2598],{},[113,2600,2602,2603],{"id":2601},"aplicando-as-cores-no-layoutcatalogvue","Aplicando as cores no ",[73,2604,2605],{},"layout\u002Fcatalog.vue",[11,2607,2608],{},"O layout da vitrine é o melhor lugar para aplicar o tema, porque ele envolve tudo que pertence à experiência pública da empresa.",[131,2610,2612],{"className":1879,"code":2611,"language":1881,"meta":136,"style":136},"\u003C!-- layouts\u002Fcatalog.vue -->\n\u003Ctemplate>\n  \u003Cdiv\n    class=\"min-h-screen bg-neutral-50 text-neutral-900 dark:bg-neutral-950 dark:text-white\"\n  >\n    \u003Cmain :style=\"themeStyle\"> \u003C!-- Estilo será herdado em todas as paginas e componentes -->\n      \u003CSharedHeader \u002F>\n      \u003Cslot \u002F>\n    \u003C\u002Fmain>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst { themeStyle } = useEmpresaTheme() \u002F\u002F Carrega o tema aqui\n\u003C\u002Fscript>\n",[73,2613,2614,2619,2627,2634,2648,2653,2677,2688,2700,2709,2718,2726,2730,2753,2772],{"__ignoreMap":136},[140,2615,2616],{"class":142,"line":143},[140,2617,2618],{"class":797},"\u003C!-- layouts\u002Fcatalog.vue -->\n",[140,2620,2621,2623,2625],{"class":142,"line":232},[140,2622,1377],{"class":290},[140,2624,1891],{"class":1890},[140,2626,1894],{"class":290},[140,2628,2629,2631],{"class":142,"line":238},[140,2630,1899],{"class":290},[140,2632,2633],{"class":1890},"div\n",[140,2635,2636,2639,2641,2643,2646],{"class":142,"line":244},[140,2637,2638],{"class":1914},"    class",[140,2640,625],{"class":290},[140,2642,304],{"class":310},[140,2644,2645],{"class":314},"min-h-screen bg-neutral-50 text-neutral-900 dark:bg-neutral-950 dark:text-white",[140,2647,339],{"class":310},[140,2649,2650],{"class":142,"line":250},[140,2651,2652],{"class":290},"  >\n",[140,2654,2655,2657,2660,2663,2665,2667,2670,2672,2674],{"class":142,"line":256},[140,2656,1909],{"class":290},[140,2658,2659],{"class":1890},"main",[140,2661,2662],{"class":1914}," :style",[140,2664,625],{"class":290},[140,2666,304],{"class":310},[140,2668,2669],{"class":314},"themeStyle",[140,2671,304],{"class":310},[140,2673,1390],{"class":290},[140,2675,2676],{"class":797}," \u003C!-- Estilo será herdado em todas as paginas e componentes -->\n",[140,2678,2679,2682,2685],{"class":142,"line":262},[140,2680,2681],{"class":290},"      \u003C",[140,2683,2684],{"class":1890},"SharedHeader",[140,2686,2687],{"class":290}," \u002F>\n",[140,2689,2690,2692,2695,2698],{"class":142,"line":466},[140,2691,2681],{"class":290},[140,2693,2694],{"class":1890},"slot",[140,2696,2204],{"class":2697},"sM3K6",[140,2699,1894],{"class":290},[140,2701,2702,2705,2707],{"class":142,"line":472},[140,2703,2704],{"class":290},"    \u003C\u002F",[140,2706,2659],{"class":1890},[140,2708,1894],{"class":290},[140,2710,2711,2713,2716],{"class":142,"line":478},[140,2712,1959],{"class":290},[140,2714,2715],{"class":1890},"div",[140,2717,1894],{"class":290},[140,2719,2720,2722,2724],{"class":142,"line":722},[140,2721,1932],{"class":290},[140,2723,1891],{"class":1890},[140,2725,1894],{"class":290},[140,2727,2728],{"class":142,"line":728},[140,2729,762],{"emptyLinePlaceholder":761},[140,2731,2732,2734,2737,2740,2743,2745,2747,2749,2751],{"class":142,"line":734},[140,2733,1377],{"class":290},[140,2735,2736],{"class":1890},"script",[140,2738,2739],{"class":1914}," setup",[140,2741,2742],{"class":1914}," lang",[140,2744,625],{"class":290},[140,2746,304],{"class":310},[140,2748,613],{"class":314},[140,2750,304],{"class":310},[140,2752,1894],{"class":290},[140,2754,2755,2757,2759,2761,2763,2765,2767,2769],{"class":142,"line":740},[140,2756,804],{"class":803},[140,2758,1121],{"class":290},[140,2760,2476],{"class":807},[140,2762,1132],{"class":290},[140,2764,811],{"class":624},[140,2766,2271],{"class":881},[140,2768,1035],{"class":620},[140,2770,2771],{"class":797},"\u002F\u002F Carrega o tema aqui\n",[140,2773,2774,2776,2778],{"class":142,"line":746},[140,2775,1932],{"class":290},[140,2777,2736],{"class":1890},[140,2779,1894],{"class":290},[11,2781,2782,2783,2785],{},"Quando o middleware salva a empresa na store, o composable lê as cores, monta o ",[73,2784,2669],{}," e o layout aplica as variáveis CSS:",[131,2787,2791],{"className":2788,"code":2789,"language":2790,"meta":136,"style":136},"language-css shiki shiki-themes material-theme-lighter github-dark github-dark","--company-primary\n--company-secondary\n","css",[73,2792,2793,2798],{"__ignoreMap":136},[140,2794,2795],{"class":142,"line":143},[140,2796,2797],{"class":620},"--company-primary\n",[140,2799,2800],{"class":142,"line":232},[140,2801,2802],{"class":620},"--company-secondary\n",[11,2804,2805],{},"A partir daí, qualquer componente dentro do layout pode usar as cores da empresa sem receber props e sem consultar a store diretamente.",[11,2807,596],{},[131,2809,2811],{"className":1879,"code":2810,"language":1881,"meta":136,"style":136},"\u003Cp class=\"text-(--company-primary)\">\n  Vitrine\n\u003C\u002Fp>\n",[73,2812,2813,2832,2837],{"__ignoreMap":136},[140,2814,2815,2817,2819,2821,2823,2825,2828,2830],{"class":142,"line":143},[140,2816,1377],{"class":290},[140,2818,11],{"class":1890},[140,2820,1915],{"class":1914},[140,2822,625],{"class":290},[140,2824,304],{"class":310},[140,2826,2827],{"class":314},"text-(--company-primary)",[140,2829,304],{"class":310},[140,2831,1894],{"class":290},[140,2833,2834],{"class":142,"line":232},[140,2835,2836],{"class":620},"  Vitrine\n",[140,2838,2839,2841,2843],{"class":142,"line":238},[140,2840,1932],{"class":290},[140,2842,11],{"class":1890},[140,2844,1894],{"class":290},[11,2846,2847],{},"Ou:",[131,2849,2851],{"className":1879,"code":2850,"language":1881,"meta":136,"style":136},"\u003Cbutton style=\"background-color: var(--company-primary)\">\n  Ver detalhes\n\u003C\u002Fbutton>\n",[73,2852,2853,2888,2893],{"__ignoreMap":136},[140,2854,2855,2857,2860,2863,2865,2867,2871,2873,2877,2879,2882,2884,2886],{"class":142,"line":143},[140,2856,1377],{"class":290},[140,2858,2859],{"class":1890},"button",[140,2861,2862],{"class":1914}," style",[140,2864,625],{"class":290},[140,2866,304],{"class":310},[140,2868,2870],{"class":2869},"seOON","background-color",[140,2872,307],{"class":290},[140,2874,2876],{"class":2875},"sUkpR"," var",[140,2878,885],{"class":290},[140,2880,2506],{"class":2881},"syox6",[140,2883,1464],{"class":290},[140,2885,304],{"class":310},[140,2887,1894],{"class":290},[140,2889,2890],{"class":142,"line":232},[140,2891,2892],{"class":620},"  Ver detalhes\n",[140,2894,2895,2897,2899],{"class":142,"line":238},[140,2896,1932],{"class":290},[140,2898,2859],{"class":1890},[140,2900,1894],{"class":290},[11,2902,2903],{},"A cor vem do banco através da empresa obtida no banco pelo middleware, é salva na store, tratada no composable e chega ao componente como CSS variable.",[11,2905,2906],{},"Esse fluxo evita acoplamento desnecessário.",[110,2908],{},[113,2910,2912],{"id":2911},"o-fluxo-completo-da-arquitetura","O fluxo completo da arquitetura",[11,2914,2915],{},"A arquitetura inicial funciona assim:",[131,2917,2919],{"className":133,"code":2918,"language":135,"meta":136,"style":136},"1. Usuário acessa \u002Flari-loja\n\n2. O Nuxt identifica o parâmetro [slug]\n   slug = \"lari-loja\"\n\n3. O middleware tenant.global.ts roda\n   valida se o slug deve ser tratado como tenant\n   chama resolver_empresa_por_slug no Supabase\n\n4. A empresa encontrada é salva na useEmpresaStore\n   empresaStore.definir(data)\n\n5. A página [slug]\u002Findex.vue consome a store\n   usa nome, descrição, logo e dados públicos da empresa\n\n6. O layout catalog.vue chama useEmpresaTheme\n   lê empresa.colors\n   aplica --company-primary e --company-secondary\n\n7. Os componentes usam as variáveis CSS\n   sem precisar saber qual empresa está ativa\n",[73,2920,2921,2926,2930,2935,2940,2944,2949,2954,2959,2963,2968,2973,2977,2982,2987,2991,2996,3001,3006,3010,3015],{"__ignoreMap":136},[140,2922,2923],{"class":142,"line":143},[140,2924,2925],{},"1. Usuário acessa \u002Flari-loja\n",[140,2927,2928],{"class":142,"line":232},[140,2929,762],{"emptyLinePlaceholder":761},[140,2931,2932],{"class":142,"line":238},[140,2933,2934],{},"2. O Nuxt identifica o parâmetro [slug]\n",[140,2936,2937],{"class":142,"line":244},[140,2938,2939],{},"   slug = \"lari-loja\"\n",[140,2941,2942],{"class":142,"line":250},[140,2943,762],{"emptyLinePlaceholder":761},[140,2945,2946],{"class":142,"line":256},[140,2947,2948],{},"3. O middleware tenant.global.ts roda\n",[140,2950,2951],{"class":142,"line":262},[140,2952,2953],{},"   valida se o slug deve ser tratado como tenant\n",[140,2955,2956],{"class":142,"line":466},[140,2957,2958],{},"   chama resolver_empresa_por_slug no Supabase\n",[140,2960,2961],{"class":142,"line":472},[140,2962,762],{"emptyLinePlaceholder":761},[140,2964,2965],{"class":142,"line":478},[140,2966,2967],{},"4. A empresa encontrada é salva na useEmpresaStore\n",[140,2969,2970],{"class":142,"line":722},[140,2971,2972],{},"   empresaStore.definir(data)\n",[140,2974,2975],{"class":142,"line":728},[140,2976,762],{"emptyLinePlaceholder":761},[140,2978,2979],{"class":142,"line":734},[140,2980,2981],{},"5. A página [slug]\u002Findex.vue consome a store\n",[140,2983,2984],{"class":142,"line":740},[140,2985,2986],{},"   usa nome, descrição, logo e dados públicos da empresa\n",[140,2988,2989],{"class":142,"line":746},[140,2990,762],{"emptyLinePlaceholder":761},[140,2992,2993],{"class":142,"line":752},[140,2994,2995],{},"6. O layout catalog.vue chama useEmpresaTheme\n",[140,2997,2998],{"class":142,"line":758},[140,2999,3000],{},"   lê empresa.colors\n",[140,3002,3003],{"class":142,"line":765},[140,3004,3005],{},"   aplica --company-primary e --company-secondary\n",[140,3007,3008],{"class":142,"line":771},[140,3009,762],{"emptyLinePlaceholder":761},[140,3011,3012],{"class":142,"line":1089},[140,3013,3014],{},"7. Os componentes usam as variáveis CSS\n",[140,3016,3017],{"class":142,"line":1105},[140,3018,3019],{},"   sem precisar saber qual empresa está ativa\n",[11,3021,3022],{},"Esse fluxo mantém cada responsabilidade em seu lugar.",[110,3024],{},[113,3026,3028],{"id":3027},"a-divisão-de-responsabilidades","A divisão de responsabilidades",[162,3030,3031,3041],{},[165,3032,3033],{},[168,3034,3035,3038],{},[171,3036,3037],{},"Camada",[171,3039,3040],{},"Responsabilidade",[178,3042,3043,3054,3062,3070,3078,3088,3101,3112],{},[168,3044,3045,3048],{},[183,3046,3047],{},"Banco",[183,3049,3050,3051,3053],{},"Relacionar dados por ",[73,3052,75],{}," e proteger acesso com RLS",[168,3055,3056,3059],{},[183,3057,3058],{},"RPC",[183,3060,3061],{},"Resolver uma empresa a partir do slug",[168,3063,3064,3067],{},[183,3065,3066],{},"Middleware",[183,3068,3069],{},"Ler o slug da URL e carregar a empresa ativa",[168,3071,3072,3075],{},[183,3073,3074],{},"Store",[183,3076,3077],{},"Guardar a empresa ativa para a aplicação",[168,3079,3080,3085],{},[183,3081,3082,3083],{},"Page ",[73,3084,1308],{},[183,3086,3087],{},"Consumir a empresa e renderizar a vitrine",[168,3089,3090,3095],{},[183,3091,3092,3093],{},"Composable ",[73,3094,2053],{},[183,3096,3097,3098,3100],{},"Transformar ",[73,3099,277],{}," em CSS variables seguras",[168,3102,3103,3109],{},[183,3104,3105,3106],{},"Layout ",[73,3107,3108],{},"catalog.vue",[183,3110,3111],{},"Aplicar as variáveis de tema na árvore da vitrine",[168,3113,3114,3117],{},[183,3115,3116],{},"Componentes",[183,3118,3119],{},"Usar as variáveis sem conhecer a lógica de tenant",[11,3121,3122],{},"Essa separação é o que deixa a arquitetura sustentável:",[58,3124,3125,3128,3133],{},[61,3126,3127],{},"Se amanhã a forma de resolver o tenant mudar, o ajuste fica no middleware ou na RPC.",[61,3129,3130,3131,53],{},"Se amanhã o formato das cores mudar, o ajuste fica no ",[73,3132,2053],{},[61,3134,3135],{},"Se amanhã a vitrine mudar visualmente, o ajuste fica na page ou nos componentes.",[11,3137,3138,3139,3146],{},"Isso é o principio ",[3140,3141,3145],"a",{"href":3142,"rel":3143},"https:\u002F\u002Flarisantos.vercel.app\u002Fblog\u002Fprincipio-solid",[3144],"nofollow","SOLID"," sendo aplicado na arquitetura frontend.",[110,3148],{},[113,3150,3152],{"id":3151},"decisões-técnicas","Decisões técnicas",[58,3154,3155,3160,3168,3174,3179,3185,3194,3208],{},[61,3156,3157,3159],{},[15,3158,24],{}," foi usado como base do frontend por organizar bem rotas, layouts, middleware, SSR e composables. Essa estrutura permite criar uma arquitetura onde o slug da URL identifica a empresa ativa sem misturar essa responsabilidade diretamente nas páginas.",[61,3161,3162,3164,3165,3167],{},[15,3163,36],{}," foi utilizado como backend principal, reunindo banco PostgreSQL, autenticação, funções RPC e Row Level Security. A decisão arquitetural foi concentrar nele a base do multitenant: dados relacionados por ",[73,3166,75],{},", tenant resolvido por slug no banco e isolamento reforçado com políticas de acesso.",[61,3169,3170,3173],{},[15,3171,3172],{},"Pinia"," foi usado para manter a empresa ativa disponível globalmente depois que o middleware resolve o slug. Assim, layouts, páginas e composables conseguem consumir o contexto do tenant sem repetir chamadas ao Supabase.",[61,3175,3176,3178],{},[15,3177,28],{}," faz parte da base visual do projeto e ajuda a construir interfaces com componentes prontos e consistentes, mantendo uma estrutura visual sólida para a vitrine pública.",[61,3180,3181,3184],{},[15,3182,3183],{},"Tailwind CSS"," foi usado para controlar o estilo da aplicação de forma flexível e produtiva. Em conjunto com CSS variables, ele permite aplicar as cores de cada empresa diretamente na interface sem criar estilos separados por tenant.",[61,3186,3187,3190,3191,3193],{},[15,3188,3189],{},"Composables"," foram usados para isolar regras reutilizáveis, como o tratamento das cores no ",[73,3192,2053],{},". Isso evita espalhar validações e transformações visuais dentro das páginas ou componentes.",[61,3195,3196,3199,3200,3202,3203,49,3205,3207],{},[15,3197,3198],{},"CSS Variables"," foram aplicadas para refletir a identidade visual de cada tenant. As cores cadastradas no campo ",[73,3201,277],{}," da empresa são transformadas em ",[73,3204,2506],{},[73,3206,2525],{},", permitindo que os componentes usem o tema da empresa sem depender diretamente da store.",[61,3209,3210,3213],{},[15,3211,3212],{},"TypeScript"," ajuda a dar mais previsibilidade aos dados da aplicação, principalmente no contrato da empresa pública consumida pelo middleware, store, layout e página dinâmica.",[110,3215],{},[113,3217,3219],{"id":3218},"conclusão","Conclusão",[11,3221,3222,3223],{},"A arquitetura inicial do projeto foi construída em cima de uma ideia simples: ",[15,3224,3225],{},"o slug define o tenant, a store compartilha o contexto e o layout reflete a identidade visual da empresa.",[58,3227,3228,3233,3236,3239,3242],{},[61,3229,3230,3231,53],{},"O banco fica como base da separação por ",[73,3232,75],{},[61,3234,3235],{},"O middleware transforma a URL em contexto real.",[61,3237,3238],{},"A store mantém esse contexto disponível.",[61,3240,3241],{},"O layout aplica as cores da empresa.",[61,3243,3244],{},"E os componentes apenas consomem o resultado.",[11,3246,3247],{},"No fim, o mais importante não foi apenas fazer funcionar, foi definir onde cada responsabilidade deveria morar e porque.",[11,3249,3250],{},"Essa é a diferença entre uma implementação que só resolve o problema de hoje e uma arquitetura que continua compreensível quando o projeto começa a crescer. Isso diferencia um desenvolvedor que somente escreve código, de um que arquiteta sistemas completos e tem visão de projeto.",[113,3252,3254],{"id":3253},"a-pergunta-que-guia-cada-decisão","A pergunta que guia cada decisão",[11,3256,3257,3258],{},"A pergunta que guiou as principais decisões foi: ",[15,3259,3260],{},"se eu precisar mudar isso daqui a seis meses, qual é o menor número de arquivos que vou precisar tocar?",[11,3262,3263],{},"Quando a resposta é \"um\", a separação está certa. Quando a resposta é \"vários espalhados\", alguma responsabilidade está no lugar errado e isso pode comprometer o projeto ou torna-lo de dficil manutenção no futuro.",[110,3265],{},[113,3267,3269],{"id":3268},"leituras-relacionadas","Leituras relacionadas",[58,3271,3272,3279,3286,3292],{},[61,3273,3274],{},[3140,3275,3278],{"href":3276,"rel":3277},"https:\u002F\u002Flarisantos.vercel.app\u002Fblog\u002Fvisitcard-generator-nuxt-pdf-do-zero",[3144],"Gerador de cartões de visita em PDF do zero com Nuxt 3",[61,3280,3281],{},[3140,3282,3285],{"href":3283,"rel":3284},"https:\u002F\u002Flarisantos.vercel.app\u002Fblog\u002Fvue-composition-vs-options-api",[3144],"Options API vs Composition API: qual usar e quando?",[61,3287,3288],{},[3140,3289,3291],{"href":3142,"rel":3290},[3144],"SOLID: 5 Princípios para Escrever Código Limpo e Escalável",[61,3293,3294],{},[3140,3295,3298],{"href":3296,"rel":3297},"https:\u002F\u002Flarisantos.vercel.app\u002Fblog\u002Fvuex-vs-pinia",[3144],"Vuex vs Pinia: Guia Completo de Gerenciamento de Estado",[3300,3301,3302],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sG-J9, html code.shiki .sG-J9{--shiki-light:#39ADB5;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .swu5b, html code.shiki .swu5b{--shiki-light:#39ADB5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sod2m, html code.shiki .sod2m{--shiki-light:#9C3EDA;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sF_wb, html code.shiki .sF_wb{--shiki-light:#39ADB5;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s0vBq, html code.shiki .s0vBq{--shiki-light:#91B859;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sMo7A, html code.shiki .sMo7A{--shiki-light:#90A4AE;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sFfmW, html code.shiki .sFfmW{--shiki-light:#39ADB5;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sFsEu, html code.shiki .sFsEu{--shiki-light:#9C3EDA;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sVPC0, html code.shiki .sVPC0{--shiki-light:#90A4AE;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s3Er8, html code.shiki .s3Er8{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sK_r7, html code.shiki .sK_r7{--shiki-light:#6182B8;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sk1zL, html code.shiki .sk1zL{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sdv8B, html code.shiki .sdv8B{--shiki-light:#E53935;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .soiBB, html code.shiki .soiBB{--shiki-light:#E2931D;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s3afY, html code.shiki .s3afY{--shiki-light:#E2931D;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sqIbZ, html code.shiki .sqIbZ{--shiki-light:#E53935;--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .s7047, html code.shiki .s7047{--shiki-light:#9C3EDA;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sMrrN, html code.shiki .sMrrN{--shiki-light:#FF5370;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sm4dI, html code.shiki .sm4dI{--shiki-light:#91B859;--shiki-default:#DBEDFF;--shiki-dark:#DBEDFF}html pre.shiki code .svU9d, html code.shiki .svU9d{--shiki-light:#39ADB5;--shiki-default:#DBEDFF;--shiki-dark:#DBEDFF}html pre.shiki code .sSJ72, html code.shiki .sSJ72{--shiki-light:#91B859;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s1Wpa, html code.shiki .s1Wpa{--shiki-light:#F76D47;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .swHuM, html code.shiki .swHuM{--shiki-light:#E53935;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sM3K6, html code.shiki .sM3K6{--shiki-light:#39ADB5;--shiki-light-font-style:inherit;--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .seOON, html code.shiki .seOON{--shiki-light:#8796B0;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sUkpR, html code.shiki .sUkpR{--shiki-light:#6182B8;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .syox6, html code.shiki .syox6{--shiki-light:#90A4AE;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}",{"title":136,"searchDepth":232,"depth":232,"links":3304},[3305,3306,3307,3308,3309,3310,3311,3313,3314,3315,3317,3318,3319,3320,3321,3322],{"id":115,"depth":232,"text":116},{"id":156,"depth":232,"text":157},{"id":411,"depth":232,"text":412},{"id":546,"depth":232,"text":547},{"id":651,"depth":232,"text":652},{"id":1314,"depth":232,"text":1315},{"id":1585,"depth":232,"text":3312},"Consumindo a store na rota [slug]\u002Findex.vue",{"id":1979,"depth":232,"text":1980},{"id":2044,"depth":232,"text":2045},{"id":2601,"depth":232,"text":3316},"Aplicando as cores no layout\u002Fcatalog.vue",{"id":2911,"depth":232,"text":2912},{"id":3027,"depth":232,"text":3028},{"id":3151,"depth":232,"text":3152},{"id":3218,"depth":232,"text":3219},{"id":3253,"depth":232,"text":3254},{"id":3268,"depth":232,"text":3269},"2026-05-14T18:12:40-03:00","Um artigo prático sobre a estratégia inicial de arquitetura de um SaaS multitenant: roteamento por slug, resolução da empresa no middleware, armazenamento em store e identidade visual dinâmica por tenant.","md","\u002Fimages\u002Fblog\u002Fmultitenant.png",{},"\u002Fblog\u002Fnuxt-saas-multitenant",{"title":5,"description":3324},"blog\u002Fnuxt-saas-multitenant",[3332,1881,3333,3334,3335,3336,3337,3338],"nuxt","arquitetura","supabase","multitenant","pinia","typescript","saas","ZNOx8j-jYe3n2EEKQt16ykA3zE02uwowmOkt1VbQSzo",1778770753295]