Services
O template separa serviços server-side (.server.ts) e client-side (.client.ts).
api.server.ts — ApiClient SSR
Cria um ApiClient configurado para uso em loaders e actions no server.
function createClient(
cloudflareEnv?: Record<string, string>,
options?: {
request?: Request;
cookies?: Record<string, string>;
} & Partial<Pick<ApiClientConfig, 'getAccessToken'>>
): ApiClient
Resolução de config:
- Tenta ler variáveis de
cloudflareEnvprimeiro - Fallback para
process.env(dev local)
Headers injetados automaticamente:
User-Agent— extraído dorequest(fallback"CactusWorker/1.0")city,country,country_alpha3— geolocalizaçãocurrency,jurisdiction— config da marcaCookie— repassado do request original (quandocookiesfornecido)
Nota: sempre usa OriginAccess.Desktop como origin access.
// Uso típico em loader:
export async function loader({ context, request }: Route.LoaderArgs) {
const client = createClient(context.cloudflare?.env, { request });
// ...
}
brand.server.ts — Brand Loading
Carrega a configuração de marca no server com cache:
function loadBrandConfig(
cloudflareEnv?: Record<string, string>
): Promise<BrandResult>
Retorno — discriminated union:
type BrandResult =
| { ok: true; brand: BrandConfig; fromCache: boolean }
| { ok: false; error: BrandError }
type BrandError = {
message: string;
status: number;
detail?: string;
}
Estratégia de cache:
- Cache-first via
getBrandCache(ttl), onde TTL vem deBRAND_CACHE_TTL(default: 3600 segundos) - No cache miss: cria
ApiClientviacreateClient, chamacreateBrandFromClient(client, { country, language }) - No erro de API: retorna
{ ok: false, error: { message, status, detail? } }
// Uso no loader:
const result = await loadBrandConfig(context.cloudflare?.env);
if (!result.ok) {
// renderiza BrandErrorPage
}
const { brand } = result;
sports.client.ts — SportsService Singleton
Singleton para integração com provedores de esportes no client-side:
function initSportsService(env: ClientEnv): SportsService
function getSportsService(): SportsService
Inicializado uma vez, retorna a mesma instância nas chamadas subsequentes.
sports.server.ts — SportsService Factory (SSR)
Factory que cria um SportsService por request no server:
function createSportsService(cloudflareEnv?: Record<string, string>): SportsService
Diferente do singleton client-side, o server cria uma instância nova a cada request para evitar estado compartilhado entre requests.
logger.server.ts — Structured Logging
Logging estruturado para uso em loaders e actions:
import { logger } from '~/services/logger.server';
logger.info('Brand loaded', { brand: brand.settings.name });
logger.error('Failed to load brand', { error });
auth.client.ts — AuthService Singleton
Singleton que inicializa o AuthService com um ApiClient client-side:
let service: AuthService | null = null;
export function initAuthService(env: ClientEnv): AuthService
export function getAuthService(): AuthService
user.client.ts — UserService Singleton
Singleton que inicializa o UserService com um ApiClient client-side. Deve ser inicializado após o usuário estar autenticado:
export function initUserService(env: ClientEnv): UserService
export function getUserService(): UserService
Os componentes da área do usuário fazem import('~/services/user.client') via dynamic import para garantir code-splitting — o bundle do UserService só é carregado quando o usuário está logado e acessa as páginas da conta.
cache.server.ts — ServerCache genérico
Classe genérica ServerCache com duas camadas de cache:
- In-memory —
Mapno Worker isolate (hot path, ~microsegundos) - CF Cache API — Cache por datacenter (warm path, ~milisegundos)
Estratégia stale-while-revalidate: serve dados do cache imediatamente e revalida em background com ctx.waitUntil().
import { ServerCache } from '~/services/cache.server';
const cache = new ServerCache({ ttl: 300 });
const data = await cache.get('key', fetchFn, ctx);
games.server.ts — Factory GamesService (SSR)
Cria um GamesService para uso em loaders SSR. Wrapper fino sobre createGamesFromClient:
import { getGamesCacheService } from '~/services/games.server';
export async function loader({ context }: Route.LoaderArgs) {
const svc = getGamesCacheService(context);
const home = await svc.getHome();
return { home };
}
games.cache.server.ts — GamesCacheService
Encapsula GamesService + ServerCache para caching automático de todos os dados de jogos:
getHome()— Homepage rows com jogos (filtra rows vazias)getBase()— Categorias + providersgetAllGames()— Lista completa paginadagetByCategory(slug)— Jogos de uma categoria específicagetByProvider(slug)— Jogos de um provider específicogetDetail(slug)— Detalhe de jogogetTopWins()/getLastWins()— Wins recentesgetStats(slug)— Estatísticassearch(term)— Busca server-side contra dados em cache (nome, tags, provider)purge(key)/purgeAll()— Invalidação manual de cache
games.client.ts — GamesService Singleton (client-side)
Singleton que inicializa o GamesService com um ApiClient client-side. Usado para chamadas diretas que não passam por cache (ex: start-game, votos):
import { getGamesClientService } from '~/services/games.client';
const games = getGamesClientService();
const vote = await games.vote(casinoGameId, true);
smartico.server.ts — Smartico Hash Generation
Gera o hash MD5 para identificação do usuário no Smartico. Executado apenas no server (loader do _layout.tsx):
import { generateSmarticoUserHash } from '~/services/smartico.server';
// No loader:
const cf = context?.cloudflare?.env;
const smarticoHash = generateSmarticoUserHash(
String(auth.user.id),
cf,
);
Internamente resolve a saltKey com prioridade: env var SMARTICO_SALT_KEY (CF secret → process.env) → smarticoHashConfig.saltKey (config file fallback).
O hash é passado ao client via loaderData e consumido pelo SmarticoInitializer para identificar o usuário no SDK Smartico. O saltKey nunca é exposto ao browser.
Separação de serviços
| Arquivo | Contexto | Token | Uso |
|---|---|---|---|
api.server.ts | Server (loader/action) | Sim (via Request cookie quando aplicável) | Brand loading, auth/profile, ações protegidas |
brand.server.ts | Server (loader) | Não | Brand config com cache (BrandResult) |
cache.server.ts | Server (loader) | Não | Cache genérico (in-memory + CF Cache API) |
sports.server.ts | Server (loader) | Não | Factory SportsService por request |
logger.server.ts | Server (loader/action) | Não | Logging estruturado |
games.server.ts | Server (loader) | Não | Factory GamesService para SSR |
games.cache.server.ts | Server (loader) | Não | GamesCacheService (cache + stale-while-revalidate) |
games.client.ts | Client (browser) | Não | GamesService singleton (start-game, votos) |
sports.client.ts | Client (browser) | Não | SportsService singleton |
auth.client.ts | Client (browser) | Não (cookie HttpOnly) | Login/logout e integração com UI |
user.client.ts | Client (browser) | Sim (via ApiClient) | Perfil, segurança, limites, IRPF, wallet |
authProfile.client.ts | Client (browser) | Não (cookie HttpOnly) | Busca profile atualizado (GET /api/auth/profile) |
validation.client.ts | Client (browser) | Não (cookie HttpOnly) | Validações: send email/SMS, verify, submit address/docs, accept terms |
userLimits.client.ts | Client (browser) | Não (cookie HttpOnly) | Atualiza limites prudenciais (POST /api/user/update-limits) |
kyc.client.ts | Client (browser) | Não (cookie HttpOnly) | Inicia KYC e consulta status via API routes |
kyc.server.ts | Server (action) | Sim (via Request cookie) | Factory KycService para server actions |
smartico.server.ts | Server (loader) | Não | Geração de hash MD5 para Smartico |
Essa separação é intencional: o token HttpOnly é lido no server (loaders/actions) e o client mantém apenas estado de UI/sessão derivado do loader.