API REST de produtividade pessoal em Spring Boot: tarefas com subtarefas, hábitos com streak e histórico, e metas com progresso, tudo atrás de autenticação JWT com proteção contra IDOR.
API no ar: Swagger UI · Health check
A API roda no plano gratuito do Render, que desliga o serviço após 15 minutos sem acesso. O primeiro acesso depois disso pode levar cerca de um minuto para responder; os seguintes são normais.
- Teste em 1 minuto
- Decisões técnicas
- Stack técnica
- Arquitetura
- Endpoints da API
- Módulo de IA
- Testes
- Como rodar localmente
- Variáveis de ambiente
- Deploy
- Licença
- Autor
Pelo navegador, sem instalar nada:
- Abra o Swagger UI.
- Em Auth →
POST /api/auth/register, clique em Try it out e envie:{ "name": "Visitante", "username": "visitante01", "email": "visitante01@example.com", "password": "senha123" } - Copie o
accessTokenda resposta, clique em Authorize (cadeado no topo) e cole o token. - Crie um hábito em
POST /api/habitse faça o check-in emPOST /api/habits/{habitId}/checkin:{ "name": "Ler 20 páginas", "frequency": "DAILY", "targetPerPeriod": 1 }
Para ver a proteção contra IDOR, crie uma segunda conta e tente buscar o hábito da primeira: a resposta é 404.
- IDOR bloqueado por design. Toda consulta de serviço é escopada por
(id, userId). Se o recurso existe mas pertence a outro usuário, a resposta é404, nunca403, para não revelar que o recurso existe. - IDs UUID, não sequenciais. Evita enumeração de recursos e inferência de volume de dados pela URL.
- Access e refresh tokens tipados. O JWT carrega um claim
type(access/refresh). Um refresh token vazado não autentica rotas protegidas, e um access token não serve para renovar a sessão. - Check-in calculado no fuso do usuário. "Hoje" vem de um
Clockinjetado combinado com o fuso salvo no perfil (padrãoAmerica/Sao_Paulo), não do fuso do servidor (UTC na nuvem). Sem isso, um check-in às 23h em São Paulo cairia no dia seguinte. OClockinjetado também deixa os testes determinísticos. - Streak por período, não por data exata. Hábitos
DAILY,WEEKLYeMONTHLYcomparam períodos: um check-in na sexta e outro na segunda seguinte contam como semanas consecutivas. - Check-in duplicado bloqueado em duas camadas. Uma checagem rápida no serviço cobre o caso comum, e uma constraint
UNIQUE (habit_id, check_in_date)no banco cobre a condição de corrida de duas requisições simultâneas. OsaveAndFlushforça oINSERTdentro dotry, para a violação virar409em vez de erro genérico. - Soft delete em hábitos. Remover um hábito marca
active = false, preservando o histórico de check-ins e o streak. - Erros padronizados. Um
@RestControllerAdviceconverte exceções de domínio, do framework e do banco no status HTTP correto (400,401,403,404,405,409,415), sempre com o mesmo corpo:status,message,timestampepath. - Schema versionado e validado. O Flyway cria o schema e o Hibernate roda com
ddl-auto: validate: se uma entidade divergir da migration, a aplicação não sobe. O teste de integração prova isso contra um PostgreSQL real.
| Camada | Tecnologia |
|---|---|
| Linguagem / Runtime | Java 21, Spring Boot 4.1 |
| Web / Segurança | Spring Web, Spring Security (stateless) |
| Persistência | Spring Data JPA, Hibernate, PostgreSQL |
| Migrações | Flyway |
| Autenticação | JWT (jjwt 0.13.0), BCrypt |
| Validação | Bean Validation |
| Documentação | SpringDoc OpenAPI 3 (Swagger UI) |
| Testes | JUnit 5, Mockito, MockMvc, Testcontainers |
| Build | Maven (via wrapper mvnw) |
| Deploy | Docker (multi-stage), Render, Neon (PostgreSQL serverless) |
| CI | GitHub Actions |
Organização por domínio (feature package), não por camada técnica:
com.lifesync.api
├── common/ # BaseEntity, ApiErrorResponse (compartilhados entre domínios)
├── config/ # SecurityConfig, OpenApiConfig, ClockConfig
├── exception/ # Exceções de domínio + GlobalExceptionHandler
├── security/ # JwtTokenProvider, JwtAuthFilter, UserDetailsServiceImpl
├── auth/ # Registro, login, refresh de token
├── user/ # Perfil do usuário autenticado (/me)
├── task/ # Tarefas + subtarefas
├── habit/ # Hábitos + histórico de check-ins
├── goal/ # Metas + histórico de progresso
├── ai/ # Plano semanal e assistente com ferramentas (cliente LLM, contexto, validação, cota, métricas)
└── LifesyncApiApplication.java
| Relação | Tipo | Comportamento no delete |
|---|---|---|
Task → User |
@ManyToOne (LAZY) |
Sem cascade: apagar usuário não afeta tarefas. |
Task → Goal |
@ManyToOne (LAZY, opcional) |
Sem cascade: apagar a meta apenas desvincula (goal = null). |
Task → SubTask |
@OneToMany (cascade = ALL, orphanRemoval = true) |
Subtarefas são apagadas junto com a tarefa. |
Habit → Goal |
@ManyToOne (LAZY, opcional) |
Sem cascade: desvincula ao apagar a meta. |
HabitHistory → Habit |
@ManyToOne (LAZY) |
Sem cascade: preservado no soft delete do hábito. |
Goal → GoalProgress |
@OneToMany (cascade = ALL, orphanRemoval = true) |
Histórico de progresso é apagado junto com a meta. |
| Versão | Conteúdo |
|---|---|
V1__create_lifesync_schema.sql |
Schema base: users, tasks, sub_tasks, habits, habit_history. |
V2__create_goals_and_link_task_habit.sql |
Cria goals e goal_progress; adiciona FK goal_id em tasks e habits. |
V3__create_ai_request_log.sql |
Cria tb_ai_request_logs: uma linha de metadados por chamada ao LLM (sem o texto do prompt nem da resposta). |
Base em produção: https://lifesync-api-ezkx.onrender.com · Base local: http://localhost:8080 · Documentação interativa: /swagger-ui/index.html
Todos os endpoints exigem Authorization: Bearer <access_token>, exceto os de /api/auth/*, o Swagger e o /actuator/health.
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/auth/register |
Registra um usuário e retorna access + refresh token |
POST |
/api/auth/login |
Autentica e retorna os tokens |
POST |
/api/auth/refresh |
Recebe o refresh token no header Authorization e retorna um novo par de tokens |
| Método | Rota | Descrição |
|---|---|---|
GET |
/api/users/me |
Retorna o perfil do usuário autenticado |
PUT |
/api/users/me |
Atualiza o perfil (nome, timezone etc.) |
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/tasks |
Cria uma tarefa (goalId opcional) |
GET |
/api/tasks/{taskId} |
Busca uma tarefa por ID |
GET |
/api/tasks |
Lista tarefas (paginado, filtro por status/prioridade) |
PUT |
/api/tasks/{taskId} |
Atualiza campos da tarefa |
DELETE |
/api/tasks/{taskId} |
Remove a tarefa (cascata para subtarefas) |
PATCH |
/api/tasks/{taskId}/status |
Atualiza apenas o status |
POST |
/api/tasks/{taskId}/subtasks |
Adiciona uma subtarefa |
PUT |
/api/tasks/{taskId}/subtasks/{subtaskId} |
Atualiza o título de uma subtarefa |
PATCH |
/api/tasks/{taskId}/subtasks/{subtaskId} |
Alterna conclusão da subtarefa |
DELETE |
/api/tasks/{taskId}/subtasks/{subtaskId} |
Remove uma subtarefa |
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/habits |
Cria um hábito (frequência + meta por período) |
GET |
/api/habits/{habitId} |
Busca um hábito por ID |
GET |
/api/habits |
Lista hábitos (paginado) |
PUT |
/api/habits/{habitId} |
Atualiza campos do hábito |
DELETE |
/api/habits/{habitId} |
Soft delete (histórico é preservado) |
POST |
/api/habits/{habitId}/checkin |
Registra o check-in de hoje (no fuso do usuário) |
GET |
/api/habits/{habitId}/history |
Histórico de check-ins (paginado) |
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/goals |
Cria uma meta com valor alvo e unidade |
GET |
/api/goals/{goalId} |
Busca uma meta por ID |
GET |
/api/goals |
Lista metas (paginado, filtro por status) |
PUT |
/api/goals/{goalId} |
Atualiza campos da meta |
DELETE |
/api/goals/{goalId} |
Remove a meta (desvincula tasks/habits, apaga histórico) |
POST |
/api/goals/{goalId}/progress |
Registra progresso (valor absoluto) |
GET |
/api/goals/{goalId}/progress |
Histórico de progresso |
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/ai/weekly-plan |
Gera o plano semanal do usuário autenticado (sem corpo). Em produção o módulo está desligado e a rota responde 503. |
POST |
/api/ai/coach |
Pergunta curta ao assistente, que consulta os dados do usuário por ferramentas somente leitura. |
POST /api/ai/weekly-plan lê as tarefas em aberto, os hábitos ativos e as metas em andamento do usuário do token, envia um resumo minimizado a um modelo de linguagem (Gemini) e devolve um plano semanal estruturado: resumo, prioridades, riscos e sugestões por dia. A resposta do modelo é tratada como entrada não confiável e só chega ao cliente depois de validada.
Estado: o módulo está desligado em produção (a rota responde 503). Quando ligado, ele só atende os usuários de uma lista de permitidos, pensada para uma conta de demonstração com dados sintéticos; o motivo está no ADR 0002. Localmente ele roda com um provedor de mentira, sem chave e sem rede.
flowchart TD
A[POST /api/ai/weekly-plan<br/>JWT: o usuário é o do token] --> B{Módulo ligado?}
B -- não --> E503[503]
B -- sim --> P{Usuário na lista<br/>de permitidos?}
P -- não --> E503
P -- sim --> C{Cota diária do usuário}
C -- esgotada --> E429[429]
C -- ok --> D[ContextBuilder<br/>só dados do usuário, minimizados,<br/>itens por índice T1, H1, G1]
D -- sem itens --> V[200 com plano vazio<br/>sem chamar o modelo]
D --> F[LlmClient: Gemini<br/>instruções em systemInstruction,<br/>dados em user_data, esquema JSON]
F -- erro ou timeout<br/>após tentativas limitadas --> E502[502 / 504]
F --> G{Validação da saída<br/>JSON, formato, índice existe,<br/>tipo coerente}
G -- inválida --> E502b[502]
G -- válida --> H[Troca índices por ids reais<br/>no servidor]
H --> I[200 com o plano]
F -.-> L[(Log e métricas<br/>só metadados)]
G -.-> L
- Cliente HTTP próprio atrás de uma interface. O Gemini é chamado por
RestClient, atrás deLlmClient; trocar de provedor é mudar uma classe. O motivo não é incompatibilidade do Spring AI, e sim controle sobre a requisição e os erros (ADR 0001). - O modelo nunca vê nem devolve ids reais. Cada item vai com um índice curto (
T1,H1,G1). O servidor guarda o mapa e só aceita índices que ele mesmo enviou, então um item inventado é rejeitado e é impossível apontar para o item de outro usuário. - Saída do modelo é entrada não confiável. O esquema JSON enviado ao provedor é só um guia; o servidor valida de novo (formato, tamanhos, tipo, índice existente). Saída inválida vira
502e é registrada, em vez de ser "consertada": a taxa de saídas inválidas é a medida de qualidade do prompt. - Falha fechada. O cliente de mentira só existe com
lifesync.ai.provider=fake; sem provider não há cliente algum e a rota responde503. Configuração incoerente (módulo ligado sem chave, por exemplo) derruba a subida da aplicação. - Lista de permitidos. Mesmo ligado, o módulo só atende os ids de usuário em
LIFESYNC_AI_ALLOWED_USERS. Lista vazia significa ninguém, um id inválido derruba a subida, e quem está fora recebe o mesmo503do módulo desligado, sem chamada ao provedor nem registro. - Cota diária calculada pelo próprio log, no fuso do usuário, com um teto em 24 horas corridas que não depende do fuso (trocar o fuso do perfil não zera a contagem).
- Erros com dono.
429é a cota do usuário,503é o módulo desligado,502é o provedor que falhou ou devolveu algo inválido,504é o provedor que demorou. Só erros passageiros (429, 5xx e timeout do provedor) são repetidos, com espera crescente e número máximo de tentativas.
O porquê de cada uma, o que foi descartado e o que ainda não foi verificado estão em docs/ai/decisoes.md.
- O que sai da API: títulos de tarefas, nomes de hábitos e títulos de metas, com prioridade, datas, sequência e progresso. Não saem: nome, e-mail, username, senha, ids e descrições.
- Títulos são texto livre, então são saneados antes do envio: sem quebras de linha nem caracteres invisíveis, tamanho e quantidade limitados, e-mails, CPFs e números longos mascarados. O mascaramento é de melhor esforço: nomes próprios e telefones curtos passam.
- Prompt injection: as instruções ficam em
systemInstruction; os dados do usuário vão separados, dentro de<user_data>, com<e>escapados para o conteúdo não fechar a tag. Nenhuma defesa de prompt é completa, por isso a saída é validada e os ids são resolvidos no servidor. - O log não guarda o texto do prompt nem da resposta, só metadados (quem, quando, modelo, tokens, duração, status). As métricas só têm números e enums.
- Chave do provedor só em variável de ambiente (
LLM_API_KEY), enviada em header, nunca em URL, log outoString. - Política de dados do provedor: na camada gratuita do Gemini o conteúdo pode ser usado para melhorar produtos e lido por revisores humanos. Por isso ela só foi usada com dados sintéticos, o módulo fica desligado em produção e, se for ligado, a lista de permitidos o restringe a uma conta de demonstração (ADR 0002).
Cada ameaça, com a mitigação, o estado e o risco que sobra, está no modelo de ameaças.
Resposta real do Gemini para um dos casos sintéticos da avaliação (um hábito diário com 21 dias de sequência e sem check-in hoje, um hábito semanal em dia e uma tarefa para dali a quatro dias); os ids são do banco de teste:
{
"summary": "Esta semana temos uma ótima oportunidade para manter a consistência nos hábitos e organizar pendências de saúde.",
"priorities": [
{ "type": "HABIT", "id": "663f36cb-ae24-4db8-b44b-c64fc3d31251", "reason": "Hábito importante com risco de perder a sequência se não for mantido." },
{ "type": "TASK", "id": "2c9dbb6b-cb96-4a6f-be1f-d6ea7246a03a", "reason": "Tarefa pendente com prazo para a metade da semana." }
],
"risks": ["Deixar de meditar e perder a sequência acumulada de dias."],
"schedule": {
"MONDAY": ["Praticar o hábito Meditar 10 minutos."],
"TUESDAY": ["Praticar o hábito Meditar 10 minutos."],
"WEDNESDAY": ["Praticar o hábito Meditar 10 minutos."],
"THURSDAY": ["Marcar consulta no dentista.", "Praticar o hábito Meditar 10 minutos."],
"FRIDAY": ["Praticar o hábito Meditar 10 minutos."],
"SATURDAY": ["Praticar o hábito Meditar 10 minutos."],
"SUNDAY": ["Praticar o hábito Meditar 10 minutos."]
}
}Sem o perfil prod, o application.yml já sobe o módulo ligado com lifesync.ai.provider=fake e a lista de permitidos aberta (*): nenhuma chamada de rede, nenhuma chave. Com a aplicação rodando (ver Como rodar localmente):
- Registre um usuário em
POST /api/auth/registere use oaccessToken. - Crie ao menos uma tarefa, um hábito ou uma meta (sem itens, a resposta é um plano vazio).
- Chame
POST /api/ai/weekly-plan, sem corpo.
O provedor de mentira devolve um plano fixo que aponta para o primeiro item de cada tipo do usuário; ele serve para exercitar o fluxo (cota, validação, ids), não para avaliar o texto. A cota local é de 5 planos por dia; a sexta chamada responde 429. Esse mesmo fluxo, com banco real, é coberto por AiWeeklyPlanFlowIntegrationTest.
POST /api/ai/coach recebe uma pergunta curta ({"question": "O que está atrasado?"}) e responde em texto. Aqui o modelo usa function calling: em vez de receber todo o contexto, ele pede os dados por três ferramentas somente leitura (tarefas_atrasadas, habitos_em_risco, progresso_das_metas).
sequenceDiagram
participant C as Cliente
participant S as CoachService
participant M as Modelo (Gemini)
C->>S: POST /api/ai/coach (JWT + pergunta)
S->>S: módulo ligado, lista de permitidos, cota
S->>S: lê o contexto do usuário do token (uma vez)
loop no máximo 4 chamadas e 45 s
S->>M: pergunta + ferramentas + passos anteriores
alt o modelo pede ferramentas
M-->>S: functionCall (nome, argumentos)
S->>S: valida nome e argumentos, executa sobre o contexto
else o modelo responde
M-->>S: texto
end
end
S->>S: valida a resposta final
S-->>C: resposta, ferramentas usadas, passos
O desenho assume que o modelo pode ser enganado por um título malicioso e garante que, mesmo assim, ele só consegue ler dados do próprio usuário:
- O loop é do servidor. O modelo só pede; quem valida, executa e decide parar é o
CoachService. - Nenhuma ferramenta aceita id de usuário ou de item. Elas filtram o contexto já montado para o usuário do token; não existe pedido que alcance outra pessoa.
- Pedido inválido encerra a conversa. Ferramenta desconhecida, argumento desconhecido (inclusive um
userId), tipo errado ou valor fora do intervalo viram erro tipado (502), sem executar nada. - Travas: 4 chamadas ao modelo por pergunta, 3 ferramentas por turno, 45 s no total, resposta final limitada e saneada.
- Mesma base do plano semanal: lista de permitidos, cota, log só com metadados (um registro por chamada), métricas e erros.
Em três perguntas reais ao Gemini (2026-11-09, dados sintéticos), as três foram respondidas: a que precisava de dados usou duas ferramentas em um turno; a que tinha instruções nos títulos apenas listou os títulos como tarefas; a fora do assunto foi recusada sem consultar nada. Formato da API, fontes, alternativas e limites no ADR 0003.
Um teste com a tag live, fora da suíte padrão e do CI, roda 10 cenários sintéticos contra o Gemini e mede taxa de saída válida, latência e tokens (como rodar). Execução de 2026-11-09, modelo gemini-3.5-flash-lite, camada gratuita:
| Rodada 1 | Rodada 2 | |
|---|---|---|
| Saídas válidas (9 casos chamam o provedor) | 9 de 9 | 9 de 9 |
| Latência: mediana / média / máxima | 1,8 s / 2,6 s / 8,2 s | 2,1 s / 3,8 s / 17,8 s |
| Tokens de entrada / saída (soma) | 7762 / 3086 | 7762 / 3187 |
A rodada 1 foi reprovada por um falso positivo da própria checagem (o plano citava o título de uma tarefa que continha o texto procurado); a checagem foi corrigida e a rodada 2 passou. No caso com instruções maliciosas nos títulos, o modelo não as obedeceu. São 18 chamadas em um dia: a medição diz que a saída é válida, não que o plano é bom. Detalhes e limites em docs/ai/decisoes.md.
307 testes, executados a cada push e pull request pelo CI (veja o badge no topo): 65 do núcleo (tabela abaixo) e 242 do módulo de IA. A maioria é unitária (Mockito, sem banco); os de integração sobem um PostgreSQL real com Testcontainers. Nenhum teste da suíte chama um LLM: o módulo de IA é testado com um servidor HTTP simulado e com o provedor de mentira.
| Classe | Tipo | Qtde. |
|---|---|---|
HabitServiceTest |
Unitário (Mockito + Clock fixo) |
19 (9 + 1 parametrizado com 10 casos de streak) |
GlobalExceptionHandlerTest |
Web (MockMvc) | 10 |
TaskServiceTest |
Unitário (Mockito) | 7 |
UserServiceTest |
Unitário (Mockito) | 7 |
JwtTokenProviderTest |
Unitário | 6 |
RegisterRequestValidationTest |
Unitário (Bean Validation) | 6 |
GoalServiceTest |
Unitário (Mockito) | 5 |
AuthControllerTest |
Fatia web (@WebMvcTest + MockMvc) |
3 |
JwtAuthFilterTest |
Unitário | 1 |
LifesyncApiApplicationTests |
Integração (Testcontainers, PostgreSQL 16) | 1 |
./mvnw clean testO teste parametrizado do HabitServiceTest cobre as regras de streak por frequência: período consecutivo, mesmo período, lacuna e troca de frequência, para DAILY, WEEKLY e MONTHLY.
O teste de integração sobe o contexto Spring completo contra um container PostgreSQL, aplica as migrações Flyway (V1 → V3) e, com ddl-auto: validate, falha se alguma entidade não bater com o schema.
Os 242 testes do módulo de IA (src/test/java/com/lifesync/api/ai) cobrem, entre outros: o assistente (ferramenta inexistente, argumento inválido, limite de passos, tempo, injeção pelos títulos, usuário A nunca recebe dados do B), a lista de permitidos (usuário fora dela recebe 503 sem chamada ao provedor; o curinga * com o provedor gemini no perfil prod derruba a subida), o cliente do Gemini contra um servidor simulado (sucesso, 429, 5xx, timeout real, corpo malformado, bloqueio, resposta cortada), a validação da saída do modelo (JSON inválido, índice inventado, tipo incoerente), o contexto (só dados do usuário, saneamento e mascaramento dos títulos), a cota por fuso, o endpoint com JWT real (401 sem token, usuário A nunca recebe item do B) e a varredura das métricas em busca de dados sensíveis.
- JDK 21+
- Docker (necessário para o teste de integração via Testcontainers)
- PostgreSQL 16+ (só para rodar a aplicação localmente)
git clone https://github.com/EndriwEngSoft/lifesync-api.git
cd lifesync-api
./mvnw clean test # roda a suíte completa (sobe Postgres via Testcontainers)
./mvnw spring-boot:run # inicia a aplicação em http://localhost:8080Sem variáveis de ambiente configuradas, a aplicação sobe com os valores de fallback de application.yml (banco local lifesync_db, senha 123456, chave JWT de desenvolvimento). Esses fallbacks existem só para facilitar o setup local. Em produção, o perfil prod (application-prod.yml) não tem fallback: tudo vem do ambiente.
| Variável | Fallback (dev) | Descrição |
|---|---|---|
SPRING_PROFILES_ACTIVE |
(nenhum) | Use prod em produção |
SPRING_DATASOURCE_URL |
jdbc:postgresql://localhost:5432/lifesync_db |
URL JDBC do banco |
SPRING_DATASOURCE_USERNAME |
postgres |
Usuário do banco |
DB_PASSWORD |
123456 |
Senha do PostgreSQL |
JWT_SECRET |
chave de dev embutida | Chave HMAC-SHA256 para assinar os tokens (mínimo 32 caracteres / 256 bits) |
JWT_EXPIRATION |
86400000 (24h) |
Validade do access token, em ms |
JWT_REFRESH_EXPIRATION |
604800000 (7 dias) |
Validade do refresh token, em ms |
APP_CORS_ALLOWED_ORIGINS |
http://localhost:3000,http://localhost:5173 |
Origens permitidas no CORS (CSV) |
PORT |
8080 |
Porta HTTP (injetada automaticamente pelo Render) |
LIFESYNC_AI_ENABLED |
true (dev) / false (prod) |
Liga o módulo de IA; desligado, a rota responde 503 |
LIFESYNC_AI_PROVIDER |
fake (dev) / vazio (prod) |
fake (sem rede) ou gemini; vazio = nenhum cliente |
LIFESYNC_AI_ALLOWED_USERS |
* (dev) / vazio (prod) |
Ids (UUID) dos usuários que podem usar a IA, separados por vírgula; vazio = ninguém. O * é só para desenvolvimento; no perfil prod com o provider gemini ele derruba a subida |
LLM_API_KEY |
(nenhum) | Chave do provedor; obrigatória só com o provider gemini |
LIFESYNC_AI_MODEL |
gemini-3.5-flash-lite |
Modelo usado pelo provider gemini |
Produção: Render (Web Service, plano free, runtime: docker) + Neon (PostgreSQL serverless).
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn -B clean package -Dmaven.test.skip=true
FROM eclipse-temurin:21-jre-alpine
VOLUME /tmp
COPY --from=build /app/target/lifesync-api.jar app.jar
ENTRYPOINT ["java", "-Xmx400m", "-jar", "/app.jar"]O primeiro estágio compila com Maven; o segundo leva só o .jar para uma imagem com JRE Alpine, sem Maven nem código-fonte. O -Xmx400m é proposital: o plano gratuito do Render tem 512 MB de RAM, e limitar o heap deixa margem para o restante da JVM e do sistema.
.github/workflows/ci.yml roda em todo push e pull request para main: configura o JDK 21 e executa ./mvnw -B clean test em ubuntu-latest, que já tem Docker, então o Testcontainers funciona sem configuração extra. O build da imagem pula os testes (-Dmaven.test.skip=true); quem roda a suíte é o CI.
O deploy no Render é definido em render.yaml (Blueprint): serviço, health check (/actuator/health) e variáveis de ambiente. As sensíveis ficam como sync: false e são preenchidas no painel, nunca no repositório. As variáveis do módulo de IA (LIFESYNC_AI_ENABLED, LIFESYNC_AI_PROVIDER, LIFESYNC_AI_ALLOWED_USERS e LLM_API_KEY) também são sync: false, sem valor no arquivo: quem manda nelas é o painel, e um sync do Blueprint não as sobrescreve. Sem elas o módulo nasce desligado e sem provider, pelos padrões do application-prod.yml. Os testes RenderBlueprintTest e ProductionAiDefaultsTest travam as duas coisas.
Distribuído sob a licença MIT. Veja LICENSE.
Endriw Colvara Bento, estudante de Engenharia de Software (Estácio), com foco em backend Java e Spring Boot.
- GitHub: github.com/EndriwEngSoft
- LinkedIn: linkedin.com/in/endriw-bento
- Portfólio: endriwdev.vercel.app