{
    "componentChunkName": "component---src-templates-medium-post-jsx",
    "path": "/pt-br/blog/coding/por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app/medium",
    "result": {"data":{"site":{"siteMetadata":{"siteUrl":"https://pablo.gg"}},"markdownRemark":{"id":"1f3f98d9-49cb-57cf-b13a-e79d05e47e62","excerpt":"Achei que tinha resolvido isso em 2023. Criei um script em Node.js que puxava dados da Google Fitness API para calcular meu TDEE, derivei os custos calóricos…","html":"<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 54.6875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAClElEQVQozwXBS0/TcAAA8H4JNcGokYT4IIAOt7GNvddubdf18e/arlvbrV3L2LqxF5PxcLghsIQAE6PGBU2QA8pBQzgYg8TERBO8SmLiwcSTF7+Dvx8UE2U7zrmZ1A1n2B7AUCZO8kKYio96cAsCHIRgoxS61Ew2FmPTVcGYBnGa5nkXk7KECMgWjF6zha87MBtMwQyDZoSgIqCKgDDxURgMYyJXX6w+andWm92VaiVLN3TaGwpZCX7Q5ofwpHrpXmQkQAZpcNONeCNwWomVp4CpAjwpofmG3pzTdfF0b+Fb3zxpxQ/bssxhXowYuGWDVjc6waQekqZQWdIk4uN25vfx4skKW1MwL+DzSw/Wlks7c8LXnrZNu3cTnt06EyRidoS4MuqCXu8/fv/u+dv9jXyj9Gye+/mhu3fQP39TW8iDgpEQy/WlWfWVGdshJ9dI535L3JxTWCnpY8XBcR/U6T4ktHxUzTkoYGrkWT93frT8ZSNRUql4AphZsVUQXlbZ3Wn081N9675opFm1WvYJ6csjLsiJMhfHEStneAFrQTCBRmoKnuCiQcBoZq6psSncXRdC6wW6KuMkGvDw6kK3Lddmr1phaHgiRJLRYlG7ExWt4dhYEB+aDNsjeIQmYSVHGEVLCAsj/igOewO+2wjbe7H556z/43Q7mRahDEcedfRfh+35ojbkQG2sClMUCwiSTzh5TTaLdyOcHWOcJD8WIiVd7W9W/33v/f3UmlFQyExF6/jEcZndmgbtVm1oAh6wY4M+ZsBFXRiP+CmeSsmYlPWCBCfQFg9MkpGjJ1MHXdkf9EKGJqKwPwOwmin21stb89mKIQAWMHIWBbwvymKAl7TMzGzFKJlIPMErUlZPGcVCvlr5D1I+9c5FyQooAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Ilustração de uma pessoa pesando comida em uma balança ao lado de uma grande equação de calorias\"\n        title=\"Ilustração de uma pessoa pesando comida em uma balança ao lado de uma grande equação de calorias\"\n        src=\"/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png\"\n        srcset=\"/static/9e01cdc27fc3fbd2671706b845005fe1/e3135/7700-calorie-illustration.png 256w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/06341/7700-calorie-illustration.png 512w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png 1024w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/ace50/7700-calorie-illustration.png 1406w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Ilustração de uma pessoa pesando comida em uma balança ao lado de uma grande equação de calorias</figcaption>\n  </figure></p>\n<p>Achei que tinha resolvido isso em 2023. Criei um <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">script em Node.js que puxava dados da Google Fitness API para calcular meu TDEE</a>, derivei os custos calóricos para construir músculo e gordura a partir de um livro de nutrição de verdade, joguei as constantes no código e considerei o problema resolvido. O eu do passado fechou o laptop muito satisfeito consigo mesmo.</p>\n<p>Aí o Google Fit começou a ser descontinuado, perdi a base sobre a qual o script estava construído e acabei criando o <a href=\"/pt-br/blog/coding/musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native/\">Musclog</a> - meu próprio app de fitness open-source. Uma das funcionalidades que eu queria eram projeções de peso: diga ao app seu objetivo e quantas calorias você está comendo, e ele te diz quanto tempo vai levar para chegar lá.</p>\n<p>Então pluguei a mesma lógica de 2023. E as projeções estavam consistentemente, confiantemente erradas.</p>\n<p>Se você já pesquisou como perder ou ganhar peso, provavelmente já ouviu a regra de ouro: <em>7700 calorias equivalem a um quilograma de gordura</em> (ou cerca de 3500 kcal por libra, se você usa o sistema americano). Essa equivalência remonta a um <a href=\"https://pubmed.ncbi.nlm.nih.gov/13594881/\" target=\"_blank\" rel=\"noreferrer\">artigo de 1958 de Max Wishnofsky</a> no <em>American Journal of Clinical Nutrition</em> - “Caloric equivalents of gained or lost weight” - e foi copiada e colada em todo app de dieta e calculadora de fitness desde então. Eu também estava usando. Acontece que esse é o problema.</p>\n<p>Fui fundo num buraco de pesquisa sobre fisiologia humana e termodinâmica para entender como modelar projeções de peso com precisão no Musclog. Aqui está o que aprendi, e o que construí da segunda vez que realmente funcionou.</p>\n<h2 id=\"quando-a-conta-nao-fecha\" style=\"position:relative;\"><a href=\"#quando-a-conta-nao-fecha\" aria-label=\"quando a conta nao fecha permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Quando a conta não fecha</h2>\n<p>Para ser transparente sobre o que é novo aqui versus o que eu <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">já cobri em 2023</a>: as constantes base - os custos calóricos para construir músculo e gordura - são as mesmas. O que é completamente novo é o modelo de P-ratio que leva em conta a experiência de treino, as equações de perda de peso de Hall-Forbes e a função Lambert W para calcular as proporções de perda de gordura versus massa magra durante um cutting. Além disso, dessa vez eu realmente rastreei os números de volta aos artigos originais em vez de só perguntar pro ChatGPT. (Eu perguntei pro ChatGPT em 2023. Publiquei a conversa como fonte. Eu sei.)</p>\n<p>Faz alguns anos que acompanho minha alimentação e composição corporal de forma obsessiva. Peso minha comida. Registro meus macros. Anoto cada treino. Eu sou esse cara, o que a essa altura já aceitei completamente.</p>\n<p>Então quando o Musclog começou a gerar projeções de peso, confiei nos dados. E os dados me disseram que após dois meses de superávit calórico moderado e treino consistente, eu deveria ter ganhado cerca de 2,5 kg.</p>\n<p>Eu tinha ganhado 1,1 kg (e sim, principalmente gordura, é o que é).</p>\n<p>A princípio pensei que estava errando na contagem de calorias. Depois achei que talvez tivesse calculado meu TDEE errado (o que provavelmente também é verdade). Mas os números continuavam errados na mesma direção, consistentemente, ao longo de vários meses. Algo estava errado no próprio modelo.</p>\n<p>Então comecei a investigar. E o que descobri foi que a regra das 7700 kcal/kg - usada por basicamente toda calculadora de calorias na internet - comete um erro fisiológico enorme que eu nunca havia questionado.</p>\n<h2 id=\"a-diferenca-entre-armazenar-e-construir\" style=\"position:relative;\"><a href=\"#a-diferenca-entre-armazenar-e-construir\" aria-label=\"a diferenca entre armazenar e construir permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>A diferença entre <em>armazenar</em> e <em>construir</em></h2>\n<p>A regra clássica das 7700 kcal/kg confunde a energia <em>contida</em> em um tecido com a energia <em>necessária para construí-lo</em>. Essas duas coisas não são a mesma.</p>\n<p>Quando você olha para os modelos termodinâmicos de síntese de tecido humano, as coisas ficam interessantes:</p>\n<p><strong>Tecido Adiposo (Gordura)</strong></p>\n<ul>\n<li>A gordura é composta por cerca de 86% de lipídios, 5% de água e um pouco de proteína.</li>\n<li>Energia armazenada em 1 kg de gordura: ~7730 kcal</li>\n<li>Custo energético para construir 1 kg de gordura: ~8840 kcal</li>\n<li>Eficiência: ~87%. O corpo humano é deprimententemente eficiente em armazenar energia excedente como gordura.</li>\n</ul>\n<p><strong>Músculo Esquelético</strong></p>\n<ul>\n<li>O músculo é composto principalmente de água (~70%), cerca de 24% de proteína e alguns lipídios em traços.</li>\n<li>Energia armazenada em 1 kg de músculo: ~1250 kcal (água tem zero calorias).</li>\n<li>Custo energético para construir 1 kg de músculo: ~3900 kcal.</li>\n<li>Eficiência: ~32%. A hipertrofia muscular é um processo caro e ineficiente - seu corpo fica constantemente quebrando e reconstruindo ligações peptídicas só para te deixar um pouquinho menos fraco.</li>\n</ul>\n<p>Se você usar a regra padrão de 7700 kcal/kg para um lean bulk, seu app vai superestimar muito o quanto o usuário vai ganhar, porque construir músculo consome uma boa parte da energia no próprio processo de construção - energia que nunca vai parar dentro do tecido.</p>\n<p>É por isso que minhas projeções estavam tão erradas. O Musclog estava assumindo que todo ganho de peso era basicamente armazenamento de gordura. Na realidade, boa parte do meu superávit estava sendo queimada como custo metabólico da construção muscular. As calorias não desapareceram - foram gastas <em>construindo</em> tecido novo, não <em>preenchendo-o</em>.</p>\n<h2 id=\"descendo-pela-toca-do-coelho\" style=\"position:relative;\"><a href=\"#descendo-pela-toca-do-coelho\" aria-label=\"descendo pela toca do coelho permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Descendo pela toca do coelho</h2>\n<p>Uma vez que entendi a distinção entre energia armazenada e custo de construção, surgiu um novo problema: de onde tiro os números reais?</p>\n<p>Comecei pelo ChatGPT (obviamente) - que, em minha defesa, também foi exatamente como obtive esses números em 2023, só que naquela vez colei a conversa com o ChatGPT diretamente no post do blog e considerei isso uma citação. Dessa vez, eu queria algo que pudesse rastrear de verdade até uma fonte real. Então acabei no <a href=\"https://books.google.com\" target=\"_blank\" rel=\"noreferrer\">Google Books</a> lendo partes de <em>The Nutritionist: Food, Nutrition, and Optimal Health</em>, do Robert Wildman, às 23h de uma terça-feira, o que definitivamente não estava nos meus planos para a semana. Encontrei lá os detalhamentos exatos de composição de tecidos que precisava. Para as porcentagens de eficiência de síntese - 23% para proteína, ~93% para lipídios (é aqui que a turma do keto começa a ficar com raiva) - rastreei esses números de volta às análises termodinâmicas de J.P. Flatt, especificamente seu capítulo de 1978 “The biochemistry of energy expenditure”, citado como fonte fundamental em vários dos artigos mais rigorosos sobre modelagem calórica. E com tudo isso, finalmente consegui fazer a matemática eu mesmo.</p>\n<p>Veja como os cálculos funcionam na prática:</p>\n<p>Para músculo, 1 kg contém 200g de proteína e 50g de lipídios. A síntese proteica roda com cerca de 23% de eficiência - ou seja, são necessários ~17 kcal para incorporar 1g de proteína ao tecido (4 kcal armazenadas, 13 kcal queimadas no processo). A síntese lipídica roda com ~93% de eficiência, então cerca de 10 kcal por grama.</p>\n<ul>\n<li>Proteína: 200 x 17 = 3400 kcal</li>\n<li>Gordura: 50 x 10 = 500 kcal</li>\n<li><strong>Total para construir 1 kg de músculo: 3900 kcal</strong></li>\n</ul>\n<p>Para gordura corporal, 1 kg contém 850g de lipídios e 20g de proteína:</p>\n<ul>\n<li>Lipídios: 850 x 10 = 8500 kcal</li>\n<li>Proteína: 20 x 17 = 340 kcal</li>\n<li><strong>Total para construir 1 kg de gordura: 8840 kcal</strong></li>\n</ul>\n<p>E essas são as constantes que foram parar no meu código. Também cruzei esses dados com os meus <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">dados do trabalho anterior de rastreamento de TDEE</a> para validar o modelo contra números reais do meu próprio corpo, o que ajudou bastante.</p>\n<h2 id=\"traduzindo-termodinamica-em-codigo\" style=\"position:relative;\"><a href=\"#traduzindo-termodinamica-em-codigo\" aria-label=\"traduzindo termodinamica em codigo permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Traduzindo termodinâmica em código</h2>\n<p>Para deixar o Musclog preciso, tive que abandonar a matemática estática e construir um modelo dinâmico. Primeiro, defini as constantes energéticas:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// Established thermodynamic model for human adipose tissue &amp; skeletal muscle</span>\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_STORED_KG_FAT</span> <span class=\"token operator\">=</span> <span class=\"token number\">7730</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_BUILD_KG_FAT</span> <span class=\"token operator\">=</span> <span class=\"token number\">8840</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_STORED_KG_MUSCLE</span> <span class=\"token operator\">=</span> <span class=\"token number\">1250</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_BUILD_KG_MUSCLE</span> <span class=\"token operator\">=</span> <span class=\"token number\">3900</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Mas conhecer as constantes não é suficiente - também precisamos saber se o usuário está ganhando principalmente gordura ou músculo. É aqui que entra o P-ratio, e honestamente o P-ratio foi a coisa que mais me surpreendeu em todo esse processo de pesquisa. O conceito foi formalizado por Gilbert Forbes em seu <a href=\"https://link.springer.com/book/10.1007/978-1-4612-4654-1\" target=\"_blank\" rel=\"noreferrer\">livro de 1987 <em>Human Body Composition</em></a> e desde então foi extensivamente estudado no contexto de intervenções de superávit e déficit calórico. Eu estava assumindo que meu bulk era aproximadamente 50/50 gordura-músculo (que é a estimativa genérica para intermediários), mas a divisão real é fortemente influenciada pela experiência de treino de formas que eu não havia considerado.</p>\n<p>Um iniciante - alguém no primeiro ano de musculação - pode ganhar músculo dramaticamente mais rápido em relação à gordura. Os ganhos de novato são reais e são fisiologicamente distintos do que acontece com um praticante intermediário ou avançado em superávit. Um levantador avançado em um bulk vai acabar armazenando proporcionalmente mais gordura porque o fator limitante para a síntese muscular já está próximo do seu teto genético. Que delícia.</p>\n<p>Escrevi uma função auxiliar para determinar essa divisão com base no nível de experiência declarado pelo usuário:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">getGainFatFraction</span><span class=\"token punctuation\">(</span>liftingExperience<span class=\"token operator\">?</span><span class=\"token operator\">:</span> LiftingExperience<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">switch</span> <span class=\"token punctuation\">(</span>liftingExperience<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">case</span> <span class=\"token string\">'beginner'</span><span class=\"token operator\">:</span>\r\n            <span class=\"token keyword\">return</span> <span class=\"token number\">0.4</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// ~60% lean gain</span>\r\n        <span class=\"token keyword\">case</span> <span class=\"token string\">'advanced'</span><span class=\"token operator\">:</span>\r\n            <span class=\"token keyword\">return</span> <span class=\"token number\">0.6</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// ~40% lean gain</span>\r\n        <span class=\"token keyword\">case</span> <span class=\"token string\">'intermediate'</span><span class=\"token operator\">:</span>\r\n        <span class=\"token keyword\">default</span><span class=\"token operator\">:</span>\r\n            <span class=\"token keyword\">return</span> <span class=\"token number\">0.5</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// 50/50</span>\r\n    <span class=\"token punctuation\">}</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Agora, em vez de dividir cegamente o superávit calórico por 7700, o Musclog calcula o custo calórico <em>efetivo</em> de ganhar um quilograma de peso corporal misto com base no nível de experiência do usuário:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">getEffectiveKcalPerKgGain</span><span class=\"token punctuation\">(</span>liftingExperience<span class=\"token operator\">?</span><span class=\"token operator\">:</span> LiftingExperience<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> fatFraction <span class=\"token operator\">=</span> <span class=\"token function\">getGainFatFraction</span><span class=\"token punctuation\">(</span>liftingExperience<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">return</span> fatFraction <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_BUILD_KG_FAT</span> <span class=\"token operator\">+</span> <span class=\"token punctuation\">(</span><span class=\"token number\">1</span> <span class=\"token operator\">-</span> fatFraction<span class=\"token punctuation\">)</span> <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_BUILD_KG_MUSCLE</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Para um iniciante: <code class=\"language-text\">(0.4 * 8840) + (0.6 * 3900) = 5.876 kcal por kg</code>. Isso é uma diferença enorme em relação à regra genérica das 7700 kcal, e é exatamente esse tipo de lacuna que explica por que minhas projeções estavam tão consistentemente erradas.</p>\n<p><img src=\"../../uploads/blog/2026/03/screenshot-6.png\" alt=\"Tela de Gerenciamento de Metas do Musclog mostrando uma meta de cutting com meta calórica diária, macros, peso alvo e percentual de gordura corporal\"></p>\n<p><em>A tela de Gerenciamento de Metas no Musclog - é aqui que o motor de projeção realmente roda. As metas calóricas e de peso que você define aqui alimentam diretamente o modelo descrito neste post.</em></p>\n<h2 id=\"a-matematica-de-perder-peso-o-modelo-hall-forbes\" style=\"position:relative;\"><a href=\"#a-matematica-de-perder-peso-o-modelo-hall-forbes\" aria-label=\"a matematica de perder peso o modelo hall forbes permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>A matemática de perder peso (o modelo Hall-Forbes)</h2>\n<p>A perda de peso exige uma abordagem completamente diferente, e é aqui que as coisas ficaram genuinamente complicadas.</p>\n<p>Quando você está em déficit calórico, está <em>liberando</em> energia armazenada em vez de construir novo tecido - então o que importa agora é a densidade energética armazenada, não o custo de construção. O problema é que você não perde gordura pura. Você perde uma mistura de gordura e massa magra, e a proporção dessa mistura muda dependendo de quão magro você já está. Quanto mais magro você está, mais massa magra você perde em relação à gordura num déficit. Esse é um fenômeno bem documentado e tem implicações reais para como calcular projeções de perda de peso.</p>\n<p>Para modelar isso com precisão, implementei as equações de densidade energética do <a href=\"https://pmc.ncbi.nlm.nih.gov/articles/PMC2376744/\" target=\"_blank\" rel=\"noreferrer\">artigo de 2008 de Kevin Hall <em>“What is the required energy deficit per unit weight loss?”</em></a> (publicado no <em>International Journal of Obesity</em>) e a curva de Forbes do livro mencionado acima. E para calcular a proporção de perda de gordura versus massa magra dinamicamente com base no percentual de gordura corporal atual, tive que implementar o ramo principal da função Lambert W em TypeScript para resolver as equações diferenciais subjacentes - uma técnica descrita no <a href=\"https://pmc.ncbi.nlm.nih.gov/articles/PMC2838532/\" target=\"_blank\" rel=\"noreferrer\">artigo de acompanhamento de Hall em 2010 <em>“Predicting metabolic adaptation, body weight, and body fat changes in humans”</em></a> no <em>American Journal of Physiology</em>:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">function</span> <span class=\"token function\">lambertW</span><span class=\"token punctuation\">(</span>z<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>z <span class=\"token operator\">&lt;</span> <span class=\"token operator\">-</span><span class=\"token number\">1</span> <span class=\"token operator\">/</span> Math<span class=\"token punctuation\">.</span><span class=\"token constant\">E</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> <span class=\"token number\">NaN</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>z <span class=\"token operator\">===</span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">let</span> w <span class=\"token operator\">=</span> z <span class=\"token operator\">&lt;</span> <span class=\"token number\">1</span> <span class=\"token operator\">?</span> z <span class=\"token operator\">:</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span>z<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">for</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">let</span> i <span class=\"token operator\">=</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span> i <span class=\"token operator\">&lt;</span> <span class=\"token number\">30</span><span class=\"token punctuation\">;</span> i<span class=\"token operator\">++</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> ew <span class=\"token operator\">=</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">exp</span><span class=\"token punctuation\">(</span>w<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">const</span> f <span class=\"token operator\">=</span> w <span class=\"token operator\">*</span> ew <span class=\"token operator\">-</span> z<span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>Math<span class=\"token punctuation\">.</span><span class=\"token function\">abs</span><span class=\"token punctuation\">(</span>f<span class=\"token punctuation\">)</span> <span class=\"token operator\">&lt;</span> <span class=\"token number\">1e-10</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> w<span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">const</span> fp <span class=\"token operator\">=</span> ew <span class=\"token operator\">*</span> <span class=\"token punctuation\">(</span>w <span class=\"token operator\">+</span> <span class=\"token number\">1</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        w <span class=\"token operator\">=</span> w <span class=\"token operator\">-</span> f <span class=\"token operator\">/</span> fp<span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n    <span class=\"token keyword\">return</span> w<span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Quero deixar claro que não derivei isso do zero - trabalhei a partir do artigo de Hall (2008) e adaptei a implementação para TypeScript. Mas validei contra dados reais das minhas próprias fases de cutting (graças à infraestrutura de rastreamento que <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">criei em 2023</a>), e o modelo se sustenta bem na prática.</p>\n<p>Com isso, o Musclog calcula dinamicamente exatamente quantas calorias estão em um quilograma de peso perdido para um usuário específico, com base no seu percentual de gordura corporal atual. Se o usuário não souber seu percentual de gordura, o app volta com segurança para a regra clássica de ~7700 kcal/kg como base - porque alguma aproximação é melhor que nenhuma.</p>\n<h2 id=\"isso-ainda-nao-e-perfeito\" style=\"position:relative;\"><a href=\"#isso-ainda-nao-e-perfeito\" aria-label=\"isso ainda nao e perfeito permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Isso ainda não é perfeito</h2>\n<p>Quero ser honesto sobre as limitações aqui. Essas são estimativas em nível populacional. A variação individual na eficiência de síntese proteica muscular, ambiente hormonal, estímulo de treino, qualidade do sono e genética significa que nenhum modelo estático vai prever perfeitamente o que acontece com o corpo de uma pessoa específica.</p>\n<p>Os valores de P-ratio que estou usando (0,4 / 0,5 / 0,6) são médias razoáveis da literatura, não números individualmente calibrados. Numa versão futura, adoraria calcular o P-ratio pessoal do usuário a partir dos seus dados históricos - o Musclog já tem tudo o que precisa para isso, só não está implementado ainda.</p>\n<p>Mas “mais preciso do que 7700 kcal/kg para tudo” é uma melhoria significativa em relação ao status quo, e as projeções ficaram visivelmente melhores desde que lancei isso.</p>\n<h2 id=\"conclusao\" style=\"position:relative;\"><a href=\"#conclusao\" aria-label=\"conclusao permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conclusão</h2>\n<p>Construir side projects é divertido porque você começa pensando “vou fazer uma calculadora simples,” e uma semana depois está lendo artigos de fisiologia dos anos 70 e implementando funções Lambert W em TypeScript à meia-noite. Clássico.</p>\n<p>Tratando o ganho de peso como um projeto de construção caro e a perda de peso como liberação dinâmica de energia, as projeções no Musclog são muito mais precisas do que as calculadoras padrão de BMR+500 espalhadas pela web. E elas realmente bateram com meus dados do mundo real depois que acertei o modelo, o que é o resultado mais satisfatório em qualquer side project.</p>\n<p>Todo esse trabalho começou com o <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">script de TDEE com a Google Fitness API que escrevi em 2023</a> - se você não leu aquele e curte esse tipo de rastreamento obsessivo de fitness, vale a pena dar uma olhada.</p>\n<p>Se quiser fuçar no código, o <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">Musclog é totalmente open-source no GitHub</a>. E se quiser usar o app de verdade, pode <a href=\"https://play.google.com/store/apps/details?id=com.werules.logger\" target=\"_blank\" rel=\"noreferrer\">baixar no Google Play</a>. Deixa um comentário se quiser conversar sobre a matemática.</p>\n<p>Até a próxima!</p>","fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTAyLTEyVDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-02-12-por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app.pt-br/","path":"/blog/coding/por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app/","locale":"pt-br"},"readingTime":{"minutes":12.01},"frontmatter":{"path":"por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app","allowComments":true,"title":"Por que a regra das 7700 calorias é falha (e como eu a corrigi no meu app)","date":"2026-02-12T00:00:00.000Z","categories":["coding"],"tags":["typescript","musclog","algorithms","fitness","nutrition","bodybuilding"],"hideExcerpt":false,"subtitle":"Spoiler: seu corpo não leu o manual"}},"categoryImage":{"childImageSharp":{"original":{"width":1920,"height":1080,"src":"/static/categories_coding-aa17d098a6a0d8e0a764a345865f5ffb.jpg"}}}},"pageContext":{"alternativeHtml":"<p></p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span class=\"gatsby-resp-image-wrapper\" style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \">\n      <span class=\"gatsby-resp-image-background-image\" style=\"padding-bottom: 54.6875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAClElEQVQozwXBS0/TcAAA8H4JNcGokYT4IIAOt7GNvddubdf18e/arlvbrV3L2LqxF5PxcLghsIQAE6PGBU2QA8pBQzgYg8TERBO8SmLiwcSTF7+Dvx8UE2U7zrmZ1A1n2B7AUCZO8kKYio96cAsCHIRgoxS61Ew2FmPTVcGYBnGa5nkXk7KECMgWjF6zha87MBtMwQyDZoSgIqCKgDDxURgMYyJXX6w+andWm92VaiVLN3TaGwpZCX7Q5ofwpHrpXmQkQAZpcNONeCNwWomVp4CpAjwpofmG3pzTdfF0b+Fb3zxpxQ/bssxhXowYuGWDVjc6waQekqZQWdIk4uN25vfx4skKW1MwL+DzSw/Wlks7c8LXnrZNu3cTnt06EyRidoS4MuqCXu8/fv/u+dv9jXyj9Gye+/mhu3fQP39TW8iDgpEQy/WlWfWVGdshJ9dI535L3JxTWCnpY8XBcR/U6T4ktHxUzTkoYGrkWT93frT8ZSNRUql4AphZsVUQXlbZ3Wn081N9675opFm1WvYJ6csjLsiJMhfHEStneAFrQTCBRmoKnuCiQcBoZq6psSncXRdC6wW6KuMkGvDw6kK3Lddmr1phaHgiRJLRYlG7ExWt4dhYEB+aDNsjeIQmYSVHGEVLCAsj/igOewO+2wjbe7H556z/43Q7mRahDEcedfRfh+35ojbkQG2sClMUCwiSTzh5TTaLdyOcHWOcJD8WIiVd7W9W/33v/f3UmlFQyExF6/jEcZndmgbtVm1oAh6wY4M+ZsBFXRiP+CmeSsmYlPWCBCfQFg9MkpGjJ1MHXdkf9EKGJqKwPwOwmin21stb89mKIQAWMHIWBbwvymKAl7TMzGzFKJlIPMErUlZPGcVCvlr5D1I+9c5FyQooAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"></span>\n  <img class=\"gatsby-resp-image-image\" alt=\"Ilustração de uma pessoa pesando comida em uma balança ao lado de uma grande equação de calorias\" title=\"Ilustração de uma pessoa pesando comida em uma balança ao lado de uma grande equação de calorias\" src=\"/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png\" srcset=\"/static/9e01cdc27fc3fbd2671706b845005fe1/e3135/7700-calorie-illustration.png 256w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/06341/7700-calorie-illustration.png 512w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png 1024w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/ace50/7700-calorie-illustration.png 1406w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\" loading=\"lazy\" decoding=\"async\">\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Ilustração de uma pessoa pesando comida em uma balança ao lado de uma grande equação de calorias</figcaption>\n  </figure><p></p>\n<p>Achei que tinha resolvido isso em 2023. Criei um <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">script em Node.js que puxava dados da Google Fitness API para calcular meu TDEE</a>, derivei os custos calóricos para construir músculo e gordura a partir de um livro de nutrição de verdade, joguei as constantes no código e considerei o problema resolvido. O eu do passado fechou o laptop muito satisfeito consigo mesmo.</p>\n<p>Aí o Google Fit começou a ser descontinuado, perdi a base sobre a qual o script estava construído e acabei criando o <a href=\"/pt-br/blog/coding/musclog-aproveitando-minha-experiencia-com-reactjs-para-criar-um-app-em-react-native/\">Musclog</a> - meu próprio app de fitness open-source. Uma das funcionalidades que eu queria eram projeções de peso: diga ao app seu objetivo e quantas calorias você está comendo, e ele te diz quanto tempo vai levar para chegar lá.</p>\n<p>Então pluguei a mesma lógica de 2023. E as projeções estavam consistentemente, confiantemente erradas.</p>\n<p>Se você já pesquisou como perder ou ganhar peso, provavelmente já ouviu a regra de ouro: <em>7700 calorias equivalem a um quilograma de gordura</em> (ou cerca de 3500 kcal por libra, se você usa o sistema americano). Essa equivalência remonta a um <a href=\"https://pubmed.ncbi.nlm.nih.gov/13594881/\" target=\"_blank\" rel=\"noreferrer\">artigo de 1958 de Max Wishnofsky</a> no <em>American Journal of Clinical Nutrition</em> - “Caloric equivalents of gained or lost weight” - e foi copiada e colada em todo app de dieta e calculadora de fitness desde então. Eu também estava usando. Acontece que esse é o problema.</p>\n<p>Fui fundo num buraco de pesquisa sobre fisiologia humana e termodinâmica para entender como modelar projeções de peso com precisão no Musclog. Aqui está o que aprendi, e o que construí da segunda vez que realmente funcionou.</p>\n<h2 id=\"quando-a-conta-nao-fecha\" style=\"position:relative;\"><a href=\"#quando-a-conta-nao-fecha\" aria-label=\"quando a conta nao fecha permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Quando a conta não fecha</h2>\n<p>Para ser transparente sobre o que é novo aqui versus o que eu <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">já cobri em 2023</a>: as constantes base - os custos calóricos para construir músculo e gordura - são as mesmas. O que é completamente novo é o modelo de P-ratio que leva em conta a experiência de treino, as equações de perda de peso de Hall-Forbes e a função Lambert W para calcular as proporções de perda de gordura versus massa magra durante um cutting. Além disso, dessa vez eu realmente rastreei os números de volta aos artigos originais em vez de só perguntar pro ChatGPT. (Eu perguntei pro ChatGPT em 2023. Publiquei a conversa como fonte. Eu sei.)</p>\n<p>Faz alguns anos que acompanho minha alimentação e composição corporal de forma obsessiva. Peso minha comida. Registro meus macros. Anoto cada treino. Eu sou esse cara, o que a essa altura já aceitei completamente.</p>\n<p>Então quando o Musclog começou a gerar projeções de peso, confiei nos dados. E os dados me disseram que após dois meses de superávit calórico moderado e treino consistente, eu deveria ter ganhado cerca de 2,5 kg.</p>\n<p>Eu tinha ganhado 1,1 kg (e sim, principalmente gordura, é o que é).</p>\n<p>A princípio pensei que estava errando na contagem de calorias. Depois achei que talvez tivesse calculado meu TDEE errado (o que provavelmente também é verdade). Mas os números continuavam errados na mesma direção, consistentemente, ao longo de vários meses. Algo estava errado no próprio modelo.</p>\n<p>Então comecei a investigar. E o que descobri foi que a regra das 7700 kcal/kg - usada por basicamente toda calculadora de calorias na internet - comete um erro fisiológico enorme que eu nunca havia questionado.</p>\n<h2 id=\"a-diferenca-entre-armazenar-e-construir\" style=\"position:relative;\"><a href=\"#a-diferenca-entre-armazenar-e-construir\" aria-label=\"a diferenca entre armazenar e construir permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>A diferença entre <em>armazenar</em> e <em>construir</em></h2>\n<p>A regra clássica das 7700 kcal/kg confunde a energia <em>contida</em> em um tecido com a energia <em>necessária para construí-lo</em>. Essas duas coisas não são a mesma.</p>\n<p>Quando você olha para os modelos termodinâmicos de síntese de tecido humano, as coisas ficam interessantes:</p>\n<p><strong>Tecido Adiposo (Gordura)</strong></p>\n<ul>\n<li>A gordura é composta por cerca de 86% de lipídios, 5% de água e um pouco de proteína.</li>\n<li>Energia armazenada em 1 kg de gordura: ~7730 kcal</li>\n<li>Custo energético para construir 1 kg de gordura: ~8840 kcal</li>\n<li>Eficiência: ~87%. O corpo humano é deprimententemente eficiente em armazenar energia excedente como gordura.</li>\n</ul>\n<p><strong>Músculo Esquelético</strong></p>\n<ul>\n<li>O músculo é composto principalmente de água (~70%), cerca de 24% de proteína e alguns lipídios em traços.</li>\n<li>Energia armazenada em 1 kg de músculo: ~1250 kcal (água tem zero calorias).</li>\n<li>Custo energético para construir 1 kg de músculo: ~3900 kcal.</li>\n<li>Eficiência: ~32%. A hipertrofia muscular é um processo caro e ineficiente - seu corpo fica constantemente quebrando e reconstruindo ligações peptídicas só para te deixar um pouquinho menos fraco.</li>\n</ul>\n<p>Se você usar a regra padrão de 7700 kcal/kg para um lean bulk, seu app vai superestimar muito o quanto o usuário vai ganhar, porque construir músculo consome uma boa parte da energia no próprio processo de construção - energia que nunca vai parar dentro do tecido.</p>\n<p>É por isso que minhas projeções estavam tão erradas. O Musclog estava assumindo que todo ganho de peso era basicamente armazenamento de gordura. Na realidade, boa parte do meu superávit estava sendo queimada como custo metabólico da construção muscular. As calorias não desapareceram - foram gastas <em>construindo</em> tecido novo, não <em>preenchendo-o</em>.</p>\n<h2 id=\"descendo-pela-toca-do-coelho\" style=\"position:relative;\"><a href=\"#descendo-pela-toca-do-coelho\" aria-label=\"descendo pela toca do coelho permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Descendo pela toca do coelho</h2>\n<p>Uma vez que entendi a distinção entre energia armazenada e custo de construção, surgiu um novo problema: de onde tiro os números reais?</p>\n<p>Comecei pelo ChatGPT (obviamente) - que, em minha defesa, também foi exatamente como obtive esses números em 2023, só que naquela vez colei a conversa com o ChatGPT diretamente no post do blog e considerei isso uma citação. Dessa vez, eu queria algo que pudesse rastrear de verdade até uma fonte real. Então acabei no <a href=\"https://books.google.com\" target=\"_blank\" rel=\"noreferrer\">Google Books</a> lendo partes de <em>The Nutritionist: Food, Nutrition, and Optimal Health</em>, do Robert Wildman, às 23h de uma terça-feira, o que definitivamente não estava nos meus planos para a semana. Encontrei lá os detalhamentos exatos de composição de tecidos que precisava. Para as porcentagens de eficiência de síntese - 23% para proteína, ~93% para lipídios (é aqui que a turma do keto começa a ficar com raiva) - rastreei esses números de volta às análises termodinâmicas de J.P. Flatt, especificamente seu capítulo de 1978 “The biochemistry of energy expenditure”, citado como fonte fundamental em vários dos artigos mais rigorosos sobre modelagem calórica. E com tudo isso, finalmente consegui fazer a matemática eu mesmo.</p>\n<p>Veja como os cálculos funcionam na prática:</p>\n<p>Para músculo, 1 kg contém 200g de proteína e 50g de lipídios. A síntese proteica roda com cerca de 23% de eficiência - ou seja, são necessários ~17 kcal para incorporar 1g de proteína ao tecido (4 kcal armazenadas, 13 kcal queimadas no processo). A síntese lipídica roda com ~93% de eficiência, então cerca de 10 kcal por grama.</p>\n<ul>\n<li>Proteína: 200 x 17 = 3400 kcal</li>\n<li>Gordura: 50 x 10 = 500 kcal</li>\n<li><strong>Total para construir 1 kg de músculo: 3900 kcal</strong></li>\n</ul>\n<p>Para gordura corporal, 1 kg contém 850g de lipídios e 20g de proteína:</p>\n<ul>\n<li>Lipídios: 850 x 10 = 8500 kcal</li>\n<li>Proteína: 20 x 17 = 340 kcal</li>\n<li><strong>Total para construir 1 kg de gordura: 8840 kcal</strong></li>\n</ul>\n<p>E essas são as constantes que foram parar no meu código. Também cruzei esses dados com os meus <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">dados do trabalho anterior de rastreamento de TDEE</a> para validar o modelo contra números reais do meu próprio corpo, o que ajudou bastante.</p>\n<h2 id=\"traduzindo-termodinamica-em-codigo\" style=\"position:relative;\"><a href=\"#traduzindo-termodinamica-em-codigo\" aria-label=\"traduzindo termodinamica em codigo permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Traduzindo termodinâmica em código</h2>\n<p>Para deixar o Musclog preciso, tive que abandonar a matemática estática e construir um modelo dinâmico. Primeiro, defini as constantes energéticas:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\">https://gist.github.com/blopa/c4e96d10027d694e7cda3749c814a99f</div>\n<p>Mas conhecer as constantes não é suficiente - também precisamos saber se o usuário está ganhando principalmente gordura ou músculo. É aqui que entra o P-ratio, e honestamente o P-ratio foi a coisa que mais me surpreendeu em todo esse processo de pesquisa. O conceito foi formalizado por Gilbert Forbes em seu <a href=\"https://link.springer.com/book/10.1007/978-1-4612-4654-1\" target=\"_blank\" rel=\"noreferrer\">livro de 1987 <em>Human Body Composition</em></a> e desde então foi extensivamente estudado no contexto de intervenções de superávit e déficit calórico. Eu estava assumindo que meu bulk era aproximadamente 50/50 gordura-músculo (que é a estimativa genérica para intermediários), mas a divisão real é fortemente influenciada pela experiência de treino de formas que eu não havia considerado.</p>\n<p>Um iniciante - alguém no primeiro ano de musculação - pode ganhar músculo dramaticamente mais rápido em relação à gordura. Os ganhos de novato são reais e são fisiologicamente distintos do que acontece com um praticante intermediário ou avançado em superávit. Um levantador avançado em um bulk vai acabar armazenando proporcionalmente mais gordura porque o fator limitante para a síntese muscular já está próximo do seu teto genético. Que delícia.</p>\n<p>Escrevi uma função auxiliar para determinar essa divisão com base no nível de experiência declarado pelo usuário:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\">https://gist.github.com/blopa/260ddb7a53127a5b544c9a132c039966</div>\n<p>Agora, em vez de dividir cegamente o superávit calórico por 7700, o Musclog calcula o custo calórico <em>efetivo</em> de ganhar um quilograma de peso corporal misto com base no nível de experiência do usuário:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\">https://gist.github.com/blopa/cd7b3f7767b353395334d272af45d35e</div>\n<p>Para um iniciante: <code class=\"language-text\">(0.4 * 8840) + (0.6 * 3900) = 5.876 kcal por kg</code>. Isso é uma diferença enorme em relação à regra genérica das 7700 kcal, e é exatamente esse tipo de lacuna que explica por que minhas projeções estavam tão consistentemente erradas.</p>\n<p><img src=\"../../uploads/blog/2026/03/screenshot-6.png\" alt=\"Tela de Gerenciamento de Metas do Musclog mostrando uma meta de cutting com meta calórica diária, macros, peso alvo e percentual de gordura corporal\"></p>\n<p><em>A tela de Gerenciamento de Metas no Musclog - é aqui que o motor de projeção realmente roda. As metas calóricas e de peso que você define aqui alimentam diretamente o modelo descrito neste post.</em></p>\n<h2 id=\"a-matematica-de-perder-peso-o-modelo-hall-forbes\" style=\"position:relative;\"><a href=\"#a-matematica-de-perder-peso-o-modelo-hall-forbes\" aria-label=\"a matematica de perder peso o modelo hall forbes permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>A matemática de perder peso (o modelo Hall-Forbes)</h2>\n<p>A perda de peso exige uma abordagem completamente diferente, e é aqui que as coisas ficaram genuinamente complicadas.</p>\n<p>Quando você está em déficit calórico, está <em>liberando</em> energia armazenada em vez de construir novo tecido - então o que importa agora é a densidade energética armazenada, não o custo de construção. O problema é que você não perde gordura pura. Você perde uma mistura de gordura e massa magra, e a proporção dessa mistura muda dependendo de quão magro você já está. Quanto mais magro você está, mais massa magra você perde em relação à gordura num déficit. Esse é um fenômeno bem documentado e tem implicações reais para como calcular projeções de perda de peso.</p>\n<p>Para modelar isso com precisão, implementei as equações de densidade energética do <a href=\"https://pmc.ncbi.nlm.nih.gov/articles/PMC2376744/\" target=\"_blank\" rel=\"noreferrer\">artigo de 2008 de Kevin Hall <em>“What is the required energy deficit per unit weight loss?”</em></a> (publicado no <em>International Journal of Obesity</em>) e a curva de Forbes do livro mencionado acima. E para calcular a proporção de perda de gordura versus massa magra dinamicamente com base no percentual de gordura corporal atual, tive que implementar o ramo principal da função Lambert W em TypeScript para resolver as equações diferenciais subjacentes - uma técnica descrita no <a href=\"https://pmc.ncbi.nlm.nih.gov/articles/PMC2838532/\" target=\"_blank\" rel=\"noreferrer\">artigo de acompanhamento de Hall em 2010 <em>“Predicting metabolic adaptation, body weight, and body fat changes in humans”</em></a> no <em>American Journal of Physiology</em>:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\">https://gist.github.com/blopa/6146221878a71148e7a1f6b4191ae385</div>\n<p>Quero deixar claro que não derivei isso do zero - trabalhei a partir do artigo de Hall (2008) e adaptei a implementação para TypeScript. Mas validei contra dados reais das minhas próprias fases de cutting (graças à infraestrutura de rastreamento que <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">criei em 2023</a>), e o modelo se sustenta bem na prática.</p>\n<p>Com isso, o Musclog calcula dinamicamente exatamente quantas calorias estão em um quilograma de peso perdido para um usuário específico, com base no seu percentual de gordura corporal atual. Se o usuário não souber seu percentual de gordura, o app volta com segurança para a regra clássica de ~7700 kcal/kg como base - porque alguma aproximação é melhor que nenhuma.</p>\n<h2 id=\"isso-ainda-nao-e-perfeito\" style=\"position:relative;\"><a href=\"#isso-ainda-nao-e-perfeito\" aria-label=\"isso ainda nao e perfeito permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Isso ainda não é perfeito</h2>\n<p>Quero ser honesto sobre as limitações aqui. Essas são estimativas em nível populacional. A variação individual na eficiência de síntese proteica muscular, ambiente hormonal, estímulo de treino, qualidade do sono e genética significa que nenhum modelo estático vai prever perfeitamente o que acontece com o corpo de uma pessoa específica.</p>\n<p>Os valores de P-ratio que estou usando (0,4 / 0,5 / 0,6) são médias razoáveis da literatura, não números individualmente calibrados. Numa versão futura, adoraria calcular o P-ratio pessoal do usuário a partir dos seus dados históricos - o Musclog já tem tudo o que precisa para isso, só não está implementado ainda.</p>\n<p>Mas “mais preciso do que 7700 kcal/kg para tudo” é uma melhoria significativa em relação ao status quo, e as projeções ficaram visivelmente melhores desde que lancei isso.</p>\n<h2 id=\"conclusao\" style=\"position:relative;\"><a href=\"#conclusao\" aria-label=\"conclusao permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conclusão</h2>\n<p>Construir side projects é divertido porque você começa pensando “vou fazer uma calculadora simples,” e uma semana depois está lendo artigos de fisiologia dos anos 70 e implementando funções Lambert W em TypeScript à meia-noite. Clássico.</p>\n<p>Tratando o ganho de peso como um projeto de construção caro e a perda de peso como liberação dinâmica de energia, as projeções no Musclog são muito mais precisas do que as calculadoras padrão de BMR+500 espalhadas pela web. E elas realmente bateram com meus dados do mundo real depois que acertei o modelo, o que é o resultado mais satisfatório em qualquer side project.</p>\n<p>Todo esse trabalho começou com o <a href=\"/pt-br/blog/coding/usando-google-fitness-api-para-calcular-meu-tdee-e-mais/\">script de TDEE com a Google Fitness API que escrevi em 2023</a> - se você não leu aquele e curte esse tipo de rastreamento obsessivo de fitness, vale a pena dar uma olhada.</p>\n<p>Se quiser fuçar no código, o <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">Musclog é totalmente open-source no GitHub</a>. E se quiser usar o app de verdade, pode <a href=\"https://play.google.com/store/apps/details?id=com.werules.logger\" target=\"_blank\" rel=\"noreferrer\">baixar no Google Play</a>. Deixa um comentário se quiser conversar sobre a matemática.</p>\n<p>Até a próxima!</p>","relatedPosts":[],"otherLanguagesUrl":[],"rss":{"title":"Por que a regra das 7700 calorias é falha (e como eu a corrigi no meu app)","description":"Por que a regra das 7700 calorias é falha (e como eu a corrigi no meu app)","date":"2026-02-12T00:00:00.000Z"},"images":["/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png","../../uploads/blog/2026/03/screenshot-6.png"],"videos":[],"comments":[],"googleFormData":{},"pageType":"blogPost","categoryImage":"/categories_coding.jpg/","slug":"/2026/2026-02-12-por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app.pt-br/","locale":"pt-br","title":"Por que a regra das 7700 calorias é falha (e como eu a corrigi no meu app)","previous":{"excerpt":"I thought I had this figured out in 2023. I built a Node.js…","html":"<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 54.6875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAClElEQVQozwXBS0/TcAAA8H4JNcGokYT4IIAOt7GNvddubdf18e/arlvbrV3L2LqxF5PxcLghsIQAE6PGBU2QA8pBQzgYg8TERBO8SmLiwcSTF7+Dvx8UE2U7zrmZ1A1n2B7AUCZO8kKYio96cAsCHIRgoxS61Ew2FmPTVcGYBnGa5nkXk7KECMgWjF6zha87MBtMwQyDZoSgIqCKgDDxURgMYyJXX6w+andWm92VaiVLN3TaGwpZCX7Q5ofwpHrpXmQkQAZpcNONeCNwWomVp4CpAjwpofmG3pzTdfF0b+Fb3zxpxQ/bssxhXowYuGWDVjc6waQekqZQWdIk4uN25vfx4skKW1MwL+DzSw/Wlks7c8LXnrZNu3cTnt06EyRidoS4MuqCXu8/fv/u+dv9jXyj9Gye+/mhu3fQP39TW8iDgpEQy/WlWfWVGdshJ9dI535L3JxTWCnpY8XBcR/U6T4ktHxUzTkoYGrkWT93frT8ZSNRUql4AphZsVUQXlbZ3Wn081N9675opFm1WvYJ6csjLsiJMhfHEStneAFrQTCBRmoKnuCiQcBoZq6psSncXRdC6wW6KuMkGvDw6kK3Lddmr1phaHgiRJLRYlG7ExWt4dhYEB+aDNsjeIQmYSVHGEVLCAsj/igOewO+2wjbe7H556z/43Q7mRahDEcedfRfh+35ojbkQG2sClMUCwiSTzh5TTaLdyOcHWOcJD8WIiVd7W9W/33v/f3UmlFQyExF6/jEcZndmgbtVm1oAh6wY4M+ZsBFXRiP+CmeSsmYlPWCBCfQFg9MkpGjJ1MHXdkf9EKGJqKwPwOwmin21stb89mKIQAWMHIWBbwvymKAl7TMzGzFKJlIPMErUlZPGcVCvlr5D1I+9c5FyQooAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Illustration of a person weighing food on a scale next to a large calorie equation\"\n        title=\"Illustration of a person weighing food on a scale next to a large calorie equation\"\n        src=\"/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png\"\n        srcset=\"/static/9e01cdc27fc3fbd2671706b845005fe1/e3135/7700-calorie-illustration.png 256w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/06341/7700-calorie-illustration.png 512w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/42a19/7700-calorie-illustration.png 1024w,\n/static/9e01cdc27fc3fbd2671706b845005fe1/ace50/7700-calorie-illustration.png 1406w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Illustration of a person weighing food on a scale next to a large calorie equation</figcaption>\n  </figure></p>\n<p>I thought I had this figured out in 2023. I built a <a href=\"/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/\">Node.js script that pulled data from the Google Fitness API to calculate my TDEE</a>, derived the calorie costs for building muscle and fat from an actual nutrition textbook, committed the constants to code, and considered the problem solved. Past me closed the laptop very satisfied with himself.</p>\n<p>Then Google Fit started getting deprecated, I lost the foundation that script was built on, and I ended up building <a href=\"/en/blog/coding/musclog-leveraging-my-reactjs-experience-to-build-a-react-native-app/\">Musclog</a> - my own open-source fitness tracker. One of the features I wanted was weight projections: tell the app your goal and how many calories you’re eating, and it tells you how long it’ll take to get there.</p>\n<p>So I plugged in the same logic from 2023. And the projections were consistently, confidently wrong.</p>\n<p>If you’ve ever looked up how to lose or gain weight, you’ve probably heard the golden rule: <em>7700 calories equals one kilogram of fat</em> (or roughly 3500 kcal per pound, if you’re used to the American version). This equivalence traces back to a <a href=\"https://pubmed.ncbi.nlm.nih.gov/13594881/\" target=\"_blank\" rel=\"noreferrer\">1958 paper by Max Wishnofsky</a> in the <em>American Journal of Clinical Nutrition</em> - “Caloric equivalents of gained or lost weight” - and it’s been copy-pasted into every diet app and fitness calculator ever since. I’d been using it too. Turns out that’s the problem.</p>\n<p>I went down a massive rabbit hole of human physiology research and thermodynamics to figure out how to accurately model weight projections in Musclog. Here’s what I learned, and what I built the second time around that actually worked.</p>\n<h2 id=\"when-math-isnt-mathing\" style=\"position:relative;\"><a href=\"#when-math-isnt-mathing\" aria-label=\"when math isnt mathing permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>When math isn’t mathing</h2>\n<p>To be upfront about what’s new here vs. what I <a href=\"/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/\">already covered in 2023</a>: the base constants - the calorie costs for building muscle and fat - are the same. What’s completely new is the P-ratio model that accounts for training experience, the Hall-Forbes weight loss equations, and the Lambert W function for calculating lean-to-fat loss ratios during a cut. Also, this time I actually traced the numbers back to the original papers instead of just asking ChatGPT - I mean, the Deep Research mode from Gemini got me to these papers, but I still had to trace the numbers back to the original sources.</p>\n<p>I’ve been obsessively tracking my food and body composition for a few years now. I weigh my food. I track my macros. I log every workout. I am that guy, which at this point I’ve fully accepted.</p>\n<p>So when Musclog started generating weight projections, I trusted the data. And the data told me that after two months of a modest caloric surplus and consistent training, I should have gained about 2.5 kg.</p>\n<p>I had gained 1.1 kg (and yes, mostly fat, it is what it is).</p>\n<p>At first I thought I was miscounting calories. Then I thought maybe I have wrongly calculated my TDEE (which is probably also true). But the numbers kept being off in the same direction, consistently, across multiple months. Something wasn’t right with the model itself.</p>\n<p>So I started digging. And what I found was that the 7700 kcal/kg rule - the one used by basically every calorie calculator on the internet - makes one massive physiological mistake that I had never thought to question.</p>\n<h2 id=\"the-difference-between-storing-and-building\" style=\"position:relative;\"><a href=\"#the-difference-between-storing-and-building\" aria-label=\"the difference between storing and building permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The difference between <em>storing</em> and <em>building</em></h2>\n<p>The classic 7700 kcal/kg rule conflates the energy <em>contained</em> within a tissue with the energy <em>required to construct</em> that tissue. These are not the same thing.</p>\n<p>When you look at the thermodynamic models for human tissue synthesis, things get interesting:</p>\n<p><strong>Adipose Tissue (Fat)</strong></p>\n<ul>\n<li>Fat is about 86% lipid, 5% water, and a bit of protein.</li>\n<li>Energy stored inside 1 kg of fat: ~7730 kcal</li>\n<li>Energy cost to build 1 kg of fat: ~8840 kcal</li>\n<li>Efficiency: ~87%. The human body is depressingly efficient at storing excess energy as fat.</li>\n</ul>\n<p><strong>Skeletal Muscle</strong></p>\n<ul>\n<li>Muscle is mostly water (~70%), about 24% protein, and some trace lipids.</li>\n<li>Energy stored inside 1 kg of muscle: ~1250 kcal (water has zero calories).</li>\n<li>Energy cost to build 1 kg of muscle: ~3900 kcal.</li>\n<li>Efficiency: ~32%. Muscle hypertrophy is an expensive, inefficient process - your body is constantly breaking down and rebuilding peptide bonds just to make you slightly less weak.</li>\n</ul>\n<p>If you use the standard 7700 kcal/kg rule for a lean bulk, your app will vastly overestimate how much weight the user will gain, because building muscle consumes a huge chunk of energy in the construction process itself - energy that never ends up stored in the tissue.</p>\n<p>This is why my projections were so far off. Musclog was assuming all weight gain was basically fat storage. In reality, a good chunk of my surplus was being burned as the metabolic cost of building muscle. The calories didn’t disappear - they were spent <em>constructing</em> new tissue, not <em>filling</em> it.</p>\n<h2 id=\"down-the-rabbit-hole\" style=\"position:relative;\"><a href=\"#down-the-rabbit-hole\" aria-label=\"down the rabbit hole permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Down the rabbit hole</h2>\n<p>Once I understood the distinction between stored energy and construction cost, I had a new problem: where do I get the actual numbers?</p>\n<p>I started with ChatGPT (obviously) - which, in my defense, is also exactly how I sourced these numbers in 2023, except that time I pasted the ChatGPT conversation directly into the blog post and considered it a citation. This time, I wanted something I could actually trace back to a real source. So I ended up on <a href=\"https://books.google.com\" target=\"_blank\" rel=\"noreferrer\">Google Books</a> (thanks gemini) reading parts of the <em>The Nutritionist: Food, Nutrition, and Optimal Health</em> by Robert Wildman at 11pm on a Tuesday, which is not something I had planned for my week. I found the exact tissue composition breakdowns I needed there. For the synthesis efficiency percentages - 23% for protein, ~93% for lipids (and here is when the keto gang starts to get angry) - I traced those back to the thermodynamic analyses by J.P. Flatt, specifically his 1978 chapter “The biochemistry of energy expenditure,” which several of the more rigorous calorie modeling papers cite as a foundational source. And from all of that, I could finally do the math myself.</p>\n<p>Here’s how the calculations actually work:</p>\n<p>For muscle, 1 kg contains 200g of protein and 50g of lipids. Protein synthesis runs at about 23% efficiency - meaning it takes ~17 kcal to incorporate 1g of protein into tissue (4 kcal stored, 13 kcal burned in the process). Lipid synthesis runs at ~93% efficiency, so about 10 kcal per gram.</p>\n<ul>\n<li>Protein: 200 x 17 = 3400 kcal</li>\n<li>Fat: 50 x 10 = 500 kcal</li>\n<li><strong>Total to build 1 kg of muscle: 3900 kcal</strong></li>\n</ul>\n<p>For body fat, 1 kg contains 850g of lipids and 20g of protein:</p>\n<ul>\n<li>Lipids: 850 x 10 = 8500 kcal</li>\n<li>Protein: 20 x 17 = 340 kcal</li>\n<li><strong>Total to build 1 kg of fat: 8840 kcal</strong></li>\n</ul>\n<p>And there are the constants that ended up in my code. I also cross-referenced these against the data from my <a href=\"/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/\">earlier TDEE tracking work</a> to sanity-check the model against real numbers from my own body, which helped a lot.</p>\n<h2 id=\"translating-thermodynamics-into-code\" style=\"position:relative;\"><a href=\"#translating-thermodynamics-into-code\" aria-label=\"translating thermodynamics into code permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Translating thermodynamics into code</h2>\n<p>To make Musclog accurate, I had to ditch the static math and build a dynamic model. First, I set up the energetic constants:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// Established thermodynamic model for human adipose tissue &amp; skeletal muscle</span>\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_STORED_KG_FAT</span> <span class=\"token operator\">=</span> <span class=\"token number\">7730</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_BUILD_KG_FAT</span> <span class=\"token operator\">=</span> <span class=\"token number\">8840</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_STORED_KG_MUSCLE</span> <span class=\"token operator\">=</span> <span class=\"token number\">1250</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">CALORIES_BUILD_KG_MUSCLE</span> <span class=\"token operator\">=</span> <span class=\"token number\">3900</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>But knowing the constants isn’t enough - we also need to know whether the user is primarily gaining fat or muscle. This is where the P-ratio comes in, and honestly the P-ratio was the thing that surprised me most in this whole research process. The concept was formalized by Gilbert Forbes in his <a href=\"https://link.springer.com/book/10.1007/978-1-4612-4654-1\" target=\"_blank\" rel=\"noreferrer\">1987 book <em>Human Body Composition</em></a> and has since been studied extensively in the context of caloric surplus and deficit interventions. I’d been assuming my bulk was roughly 50/50 fat-to-muscle (which is the generic intermediate estimate), but the actual split is heavily influenced by training experience in ways I hadn’t accounted for.</p>\n<p>A beginner - someone in their first year of lifting - can gain muscle dramatically faster relative to fat. Newbie gains are real, and they’re physiologically distinct from what happens to an intermediate or advanced lifter in a surplus. An advanced lifter in a bulk will end up storing proportionally more fat because the rate-limiting factor for muscle synthesis is already close to their genetic ceiling. Fun times.</p>\n<p>I wrote a helper to determine this split based on the user’s self-reported experience level:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">getGainFatFraction</span><span class=\"token punctuation\">(</span>liftingExperience<span class=\"token operator\">?</span><span class=\"token operator\">:</span> LiftingExperience<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">switch</span> <span class=\"token punctuation\">(</span>liftingExperience<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">case</span> <span class=\"token string\">'beginner'</span><span class=\"token operator\">:</span>\r\n            <span class=\"token keyword\">return</span> <span class=\"token number\">0.4</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// ~60% lean gain</span>\r\n        <span class=\"token keyword\">case</span> <span class=\"token string\">'advanced'</span><span class=\"token operator\">:</span>\r\n            <span class=\"token keyword\">return</span> <span class=\"token number\">0.6</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// ~40% lean gain</span>\r\n        <span class=\"token keyword\">case</span> <span class=\"token string\">'intermediate'</span><span class=\"token operator\">:</span>\r\n        <span class=\"token keyword\">default</span><span class=\"token operator\">:</span>\r\n            <span class=\"token keyword\">return</span> <span class=\"token number\">0.5</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// 50/50</span>\r\n    <span class=\"token punctuation\">}</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Now, instead of blindly dividing a caloric surplus by 7700, Musclog calculates the <em>effective</em> caloric cost of gaining a kilogram of mixed body weight based on the user’s experience level:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">getEffectiveKcalPerKgGain</span><span class=\"token punctuation\">(</span>liftingExperience<span class=\"token operator\">?</span><span class=\"token operator\">:</span> LiftingExperience<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> fatFraction <span class=\"token operator\">=</span> <span class=\"token function\">getGainFatFraction</span><span class=\"token punctuation\">(</span>liftingExperience<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">return</span> fatFraction <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_BUILD_KG_FAT</span> <span class=\"token operator\">+</span> <span class=\"token punctuation\">(</span><span class=\"token number\">1</span> <span class=\"token operator\">-</span> fatFraction<span class=\"token punctuation\">)</span> <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_BUILD_KG_MUSCLE</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>For a beginner: <code class=\"language-text\">(0.4 * 8840) + (0.6 * 3900) = 5,876 kcal per kg</code>. That is a massive difference from the generic 7700 kcal rule, and it’s exactly the kind of gap that explains why my projections were so consistently wrong.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAArCAIAAAD3xz8iAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEv0lEQVRIx+2VS28bVRTHQ7uI34953Oe5zxmPHzMe23HipHk6aZJGtA1tqFTxDQpih1QkdsAHYFPxBVggFrBi1QUSEjvY8YXQtRPXbipVYoeE9NfoztH8fO85556/1+qR9Ns27LY2TqfDk4Px6XRycbpxcjg+Ox6cHPYP79UjVZC4quPjx68vv/jr8rvX5z/8fPbHj/rVi7WalV7i4L1HFzsXp5vnx7uPHpw8fXxy9dHG6XTj/mEjXsC/PXn599Wr3x/+9OvjP3/R33+6VpS0IMi6wHeId4f4d4j/AZ4tsHeX+ndpUJS0pHhRkqqNaq20nLTLnVYxi8sW1kqKVQzUrGxEqh7LeiRrVlYNVA1UZiopNhMvuycta1qJoWJ5WXMH036qNjfEKOfDvhjlKO0UFbutkmZFScs9VT/vVXfikmQOrilWB+IkaB1IQ9B3qg7XasyQkprBPlBpDdcSA5eRpZIHFIUMLxQwTAAfbbOHU3a4TZnkIaflOewBGW6Nt/Z2nn3y/OzDCyyYT1EwYxYKOQZNpCFCE/dznDi4rHmNBJdXT158/tnX337z8qsv+b12uKFDTlZ4jj2Km8QpWMBuZ04QJ1RB1W80cRAACYD4BPkEBRTP5VPkjnPzugQDCRgJgSJgc8bDIQKGgHokvMaYO8h87V6Xd96dHk3294mEkBOPhJ08O7h/nyqYJ+9TtHN40Eq77lzzdJbhbp5GnZZHAp+hBvJVy3QGmUfDecIeCdv9royNi/BbOfuKBAmYtB3lvXiQmqwTaIYt4ETQREV5T3ZbJJIuYgEbuIE18yimw4gd96YXZ4O9bZUmOmvTlsIG0MCYSXp0fhYNUxeJhJMVKwXDGrAVKk1EJwo1Cw2ff4c0p7FSvRa6idyCGcFdRbZaO9ODaNgLLSexxLHAGtB+YqaDe8dHNFFoAUdLsA8EK0ZiEWVtHiukOTYuMXecRLC2jrI2seI66OL8DdyQ7nr7QBoMe9wtvIW4U4Mibzk4G57rwShIWlzVzQy/Rw6GQSaGfdrvilFuNkd+O3Jzq/mtr/mNluCqhaoVNSsrxi1Kiq0DXge8egRaEGiuRXytIAhJIkiiqpVilNM8FaNcjgZ2a1xzvkmLihckaSaZGl+o8QM1fhB0BxUrnJMUJCWxgSQuKdaUvEZRU7BAQWikJ1kD6EzEkwKZODRRqKMGZzVBFgUjbgeO8vHw4+fPRpMx0zJk2CfhwlICGvok8LBHBM03N0RsioKsXaekeQlwq9fZ2ttNsjTJUttpmyQGo5ZdxadIRKbT73OrSpI6uKx4STtnbQjqSeYrHhoRahFoCDQ0gS7kCacmkNqiz873XWFcw50EXXd/A6Rwq+frkq0LVpAzJ1azguHYspataOCDjPa7MOzzQabHw5qVC7goWc2yfALb+yAzUTGypKhrFUti1Wm5S25kg1NfQagFssqTfNm3PUl5zCCmgUTVt48NuJunl1dPxzsTpoQzEBwEqx7sYRQCb+f5m4KV5gUDHHfbk73dbt7v9DMVW5O0lj181jCkYjvY3JSxXWqVqzlrCuoL7kvARiItsFWBZB7glXniuEaCGtxckrdUnCWyaME75ulmZlbg8kyl1ed7RvJf63/4PwP/Azt4INPFaRKbAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog Goals Management screen showing a cutting goal with daily calorie target, macros, target weight and body fat percentage\"\n        title=\"Musclog Goals Management screen showing a cutting goal with daily calorie target, macros, target weight and body fat percentage\"\n        src=\"/static/881f67e91e8516dba6fc2112cbe0aab8/673a2/musclog-goals-management.png\"\n        srcset=\"/static/881f67e91e8516dba6fc2112cbe0aab8/e3135/musclog-goals-management.png 256w,\n/static/881f67e91e8516dba6fc2112cbe0aab8/06341/musclog-goals-management.png 512w,\n/static/881f67e91e8516dba6fc2112cbe0aab8/673a2/musclog-goals-management.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog Goals Management screen showing a cutting goal with daily calorie target, macros, target weight and body fat percentage</figcaption>\n  </figure></p>\n<p><em>The Goals Management screen in Musclog - this is where the projection engine actually runs. The calorie targets and weight goals you set here feed directly into the model described in this post.</em></p>\n<h2 id=\"the-math-of-losing-weight-the-hall-forbes-model\" style=\"position:relative;\"><a href=\"#the-math-of-losing-weight-the-hall-forbes-model\" aria-label=\"the math of losing weight the hall forbes model permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The math of losing weight (the Hall-Forbes model)</h2>\n<p>Weight loss requires a completely different approach, and this is where things got genuinely complicated.</p>\n<p>When you’re in a caloric deficit, you’re <em>liberating</em> stored energy rather than constructing new tissue - so the stored energy density is what matters now, not the construction cost. The problem is that you’re not losing pure fat. You lose a mix of fat and lean mass, and the ratio of that mix shifts depending on how lean you already are. The leaner you are, the more lean mass you’ll lose relative to fat in a deficit. This is a well-documented phenomenon and it has real implications for how you should calculate weight loss projections.</p>\n<p>To model this accurately, I implemented the energy density equations from <a href=\"https://pmc.ncbi.nlm.nih.gov/articles/PMC2376744/\" target=\"_blank\" rel=\"noreferrer\">Kevin Hall’s 2008 paper <em>“What is the required energy deficit per unit weight loss?”</em></a> (published in the <em>International Journal of Obesity</em>) and the Forbes curve from the book mentioned above. And to calculate the lean-to-fat loss ratio dynamically based on current body fat percentage, I had to implement the Principal branch of the Lambert W function in TypeScript to solve the underlying differential equations - a technique described in <a href=\"https://pmc.ncbi.nlm.nih.gov/articles/PMC2838532/\" target=\"_blank\" rel=\"noreferrer\">Hall’s follow-up 2010 paper <em>“Predicting metabolic adaptation, body weight, and body fat changes in humans”</em></a> in the <em>American Journal of Physiology</em>:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">function</span> <span class=\"token function\">lambertW</span><span class=\"token punctuation\">(</span>z<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>z <span class=\"token operator\">&lt;</span> <span class=\"token operator\">-</span><span class=\"token number\">1</span> <span class=\"token operator\">/</span> Math<span class=\"token punctuation\">.</span><span class=\"token constant\">E</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> <span class=\"token number\">NaN</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>z <span class=\"token operator\">===</span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">let</span> w <span class=\"token operator\">=</span> z <span class=\"token operator\">&lt;</span> <span class=\"token number\">1</span> <span class=\"token operator\">?</span> z <span class=\"token operator\">:</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span>z<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">for</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">let</span> i <span class=\"token operator\">=</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span> i <span class=\"token operator\">&lt;</span> <span class=\"token number\">30</span><span class=\"token punctuation\">;</span> i<span class=\"token operator\">++</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> ew <span class=\"token operator\">=</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">exp</span><span class=\"token punctuation\">(</span>w<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">const</span> f <span class=\"token operator\">=</span> w <span class=\"token operator\">*</span> ew <span class=\"token operator\">-</span> z<span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>Math<span class=\"token punctuation\">.</span><span class=\"token function\">abs</span><span class=\"token punctuation\">(</span>f<span class=\"token punctuation\">)</span> <span class=\"token operator\">&lt;</span> <span class=\"token number\">1e-10</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> w<span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">const</span> fp <span class=\"token operator\">=</span> ew <span class=\"token operator\">*</span> <span class=\"token punctuation\">(</span>w <span class=\"token operator\">+</span> <span class=\"token number\">1</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        w <span class=\"token operator\">=</span> w <span class=\"token operator\">-</span> f <span class=\"token operator\">/</span> fp<span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n    <span class=\"token keyword\">return</span> w<span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>I want to be upfront that I did not sit down and derive this from scratch - I worked from the Hall (2008) paper and adapted the implementation for TypeScript. But I did validate it against real data from my own cutting phases (courtesy of the tracking infrastructure I <a href=\"/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/\">built back in 2023</a>), and the model holds up well in practice.</p>\n<p>Using this, Musclog dynamically calculates exactly how many calories are in a kilogram of weight lost for a specific user, based on their current body fat percentage. If a user doesn’t know their body fat, the app safely falls back to the classic ~7700 kcal/kg rule as a baseline - because some approximation beats no approximation.</p>\n<h2 id=\"this-still-isnt-perfect\" style=\"position:relative;\"><a href=\"#this-still-isnt-perfect\" aria-label=\"this still isnt perfect permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>This still isn’t perfect</h2>\n<p>I want to be honest about the limits here. These are population-level estimates. Individual variation in muscle protein synthesis efficiency, hormonal environment, training stimulus, sleep quality, and genetics means that no static model will perfectly predict what happens to any specific person’s body.</p>\n<p>The P-ratio values I’m using (0.4 / 0.5 / 0.6) are reasonable averages from the literature, not individually calibrated numbers. In a future version, I’d love to calculate a user’s personal P-ratio from their historical data - Musclog already has everything it needs for that, it’s just not implemented yet.</p>\n<p>But “more accurate than 7700 kcal/kg for everything” is a meaningful improvement over the status quo, and the projections have been noticeably better since I shipped this.</p>\n<h2 id=\"conclusion\" style=\"position:relative;\"><a href=\"#conclusion\" aria-label=\"conclusion permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conclusion</h2>\n<p>Building side projects is fun because you start off thinking “I’ll just make a simple calculator,” and a week later you’re reading 1970s physiology papers and implementing Lambert W functions in TypeScript at midnight. Classic.</p>\n<p>By treating weight gain as an expensive construction project and weight loss as dynamic energy liberation, the projections in Musclog are vastly more accurate than the standard BMR+500 calculators floating around the web. And they actually matched my real-world data once I got it right, which is the most satisfying outcome in any side project.</p>\n<p>This whole line of work started with the <a href=\"/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/\">Google Fitness API TDEE script I wrote in 2023</a> - if you haven’t read that one and you’re into this kind of obsessive fitness tracking, it’s worth a look.</p>\n<p>If you want to poke around the code, <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">Musclog is fully open-source on GitHub</a>. And if you actually want to use it, you can <a href=\"https://play.google.com/store/apps/details?id=com.werules.logger\" target=\"_blank\" rel=\"noreferrer\">download it on Google Play</a>. Drop a comment if you want to geek out over the math.</p>\n<p>See you in the next one!</p>","fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTAyLTEyVDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-02-12-why-the-7700-calorie-rule-is-broken-and-how-i-fixed-it-in-my-app.en/","path":"/blog/coding/why-the-7700-calorie-rule-is-broken-and-how-i-fixed-it-in-my-app/","locale":"en"},"rawMarkdownBody":"![Illustration of a person weighing food on a scale next to a large calorie equation](../../uploads/blog/2026/02/7700-calorie-illustration.png)\r\n\r\nI thought I had this figured out in 2023. I built a [Node.js script that pulled data from the Google Fitness API to calculate my TDEE](/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/), derived the calorie costs for building muscle and fat from an actual nutrition textbook, committed the constants to code, and considered the problem solved. Past me closed the laptop very satisfied with himself.\r\n\r\nThen Google Fit started getting deprecated, I lost the foundation that script was built on, and I ended up building [Musclog](/en/blog/coding/musclog-leveraging-my-reactjs-experience-to-build-a-react-native-app/) - my own open-source fitness tracker. One of the features I wanted was weight projections: tell the app your goal and how many calories you're eating, and it tells you how long it'll take to get there.\r\n\r\nSo I plugged in the same logic from 2023. And the projections were consistently, confidently wrong.\r\n\r\nIf you've ever looked up how to lose or gain weight, you've probably heard the golden rule: *7700 calories equals one kilogram of fat* (or roughly 3500 kcal per pound, if you're used to the American version). This equivalence traces back to a [1958 paper by Max Wishnofsky](https://pubmed.ncbi.nlm.nih.gov/13594881/) in the *American Journal of Clinical Nutrition* - \"Caloric equivalents of gained or lost weight\" - and it's been copy-pasted into every diet app and fitness calculator ever since. I'd been using it too. Turns out that's the problem.\r\n\r\nI went down a massive rabbit hole of human physiology research and thermodynamics to figure out how to accurately model weight projections in Musclog. Here's what I learned, and what I built the second time around that actually worked.\r\n\r\n## When math isn't mathing\r\n\r\nTo be upfront about what's new here vs. what I [already covered in 2023](/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/): the base constants - the calorie costs for building muscle and fat - are the same. What's completely new is the P-ratio model that accounts for training experience, the Hall-Forbes weight loss equations, and the Lambert W function for calculating lean-to-fat loss ratios during a cut. Also, this time I actually traced the numbers back to the original papers instead of just asking ChatGPT - I mean, the Deep Research mode from Gemini got me to these papers, but I still had to trace the numbers back to the original sources.\r\n\r\nI've been obsessively tracking my food and body composition for a few years now. I weigh my food. I track my macros. I log every workout. I am that guy, which at this point I've fully accepted.\r\n\r\nSo when Musclog started generating weight projections, I trusted the data. And the data told me that after two months of a modest caloric surplus and consistent training, I should have gained about 2.5 kg.\r\n\r\nI had gained 1.1 kg (and yes, mostly fat, it is what it is).\r\n\r\nAt first I thought I was miscounting calories. Then I thought maybe I have wrongly calculated my TDEE (which is probably also true). But the numbers kept being off in the same direction, consistently, across multiple months. Something wasn't right with the model itself.\r\n\r\nSo I started digging. And what I found was that the 7700 kcal/kg rule - the one used by basically every calorie calculator on the internet - makes one massive physiological mistake that I had never thought to question.\r\n\r\n## The difference between *storing* and *building*\r\n\r\nThe classic 7700 kcal/kg rule conflates the energy *contained* within a tissue with the energy *required to construct* that tissue. These are not the same thing.\r\n\r\nWhen you look at the thermodynamic models for human tissue synthesis, things get interesting:\r\n\r\n**Adipose Tissue (Fat)**\r\n\r\n- Fat is about 86% lipid, 5% water, and a bit of protein.\r\n- Energy stored inside 1 kg of fat: ~7730 kcal\r\n- Energy cost to build 1 kg of fat: ~8840 kcal\r\n- Efficiency: ~87%. The human body is depressingly efficient at storing excess energy as fat.\r\n\r\n**Skeletal Muscle**\r\n\r\n- Muscle is mostly water (~70%), about 24% protein, and some trace lipids.\r\n- Energy stored inside 1 kg of muscle: ~1250 kcal (water has zero calories).\r\n- Energy cost to build 1 kg of muscle: ~3900 kcal.\r\n- Efficiency: ~32%. Muscle hypertrophy is an expensive, inefficient process - your body is constantly breaking down and rebuilding peptide bonds just to make you slightly less weak.\r\n\r\nIf you use the standard 7700 kcal/kg rule for a lean bulk, your app will vastly overestimate how much weight the user will gain, because building muscle consumes a huge chunk of energy in the construction process itself - energy that never ends up stored in the tissue.\r\n\r\nThis is why my projections were so far off. Musclog was assuming all weight gain was basically fat storage. In reality, a good chunk of my surplus was being burned as the metabolic cost of building muscle. The calories didn't disappear - they were spent *constructing* new tissue, not *filling* it.\r\n\r\n## Down the rabbit hole\r\n\r\nOnce I understood the distinction between stored energy and construction cost, I had a new problem: where do I get the actual numbers?\r\n\r\nI started with ChatGPT (obviously) - which, in my defense, is also exactly how I sourced these numbers in 2023, except that time I pasted the ChatGPT conversation directly into the blog post and considered it a citation. This time, I wanted something I could actually trace back to a real source. So I ended up on [Google Books](https://books.google.com) (thanks gemini) reading parts of the *The Nutritionist: Food, Nutrition, and Optimal Health* by Robert Wildman at 11pm on a Tuesday, which is not something I had planned for my week. I found the exact tissue composition breakdowns I needed there. For the synthesis efficiency percentages - 23% for protein, ~93% for lipids (and here is when the keto gang starts to get angry) - I traced those back to the thermodynamic analyses by J.P. Flatt, specifically his 1978 chapter \"The biochemistry of energy expenditure,\" which several of the more rigorous calorie modeling papers cite as a foundational source. And from all of that, I could finally do the math myself.\r\n\r\nHere's how the calculations actually work:\r\n\r\nFor muscle, 1 kg contains 200g of protein and 50g of lipids. Protein synthesis runs at about 23% efficiency - meaning it takes ~17 kcal to incorporate 1g of protein into tissue (4 kcal stored, 13 kcal burned in the process). Lipid synthesis runs at ~93% efficiency, so about 10 kcal per gram.\r\n\r\n- Protein: 200 x 17 = 3400 kcal\r\n- Fat: 50 x 10 = 500 kcal\r\n- **Total to build 1 kg of muscle: 3900 kcal**\r\n\r\nFor body fat, 1 kg contains 850g of lipids and 20g of protein:\r\n\r\n- Lipids: 850 x 10 = 8500 kcal\r\n- Protein: 20 x 17 = 340 kcal\r\n- **Total to build 1 kg of fat: 8840 kcal**\r\n\r\nAnd there are the constants that ended up in my code. I also cross-referenced these against the data from my [earlier TDEE tracking work](/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/) to sanity-check the model against real numbers from my own body, which helped a lot.\r\n\r\n## Translating thermodynamics into code\r\n\r\nTo make Musclog accurate, I had to ditch the static math and build a dynamic model. First, I set up the energetic constants:\r\n\r\n```typescript\r\n// Established thermodynamic model for human adipose tissue & skeletal muscle\r\nconst CALORIES_STORED_KG_FAT = 7730;\r\nconst CALORIES_BUILD_KG_FAT = 8840;\r\n\r\nconst CALORIES_STORED_KG_MUSCLE = 1250;\r\nconst CALORIES_BUILD_KG_MUSCLE = 3900;\r\n```\r\n\r\nBut knowing the constants isn't enough - we also need to know whether the user is primarily gaining fat or muscle. This is where the P-ratio comes in, and honestly the P-ratio was the thing that surprised me most in this whole research process. The concept was formalized by Gilbert Forbes in his [1987 book *Human Body Composition*](https://link.springer.com/book/10.1007/978-1-4612-4654-1) and has since been studied extensively in the context of caloric surplus and deficit interventions. I'd been assuming my bulk was roughly 50/50 fat-to-muscle (which is the generic intermediate estimate), but the actual split is heavily influenced by training experience in ways I hadn't accounted for.\r\n\r\nA beginner - someone in their first year of lifting - can gain muscle dramatically faster relative to fat. Newbie gains are real, and they're physiologically distinct from what happens to an intermediate or advanced lifter in a surplus. An advanced lifter in a bulk will end up storing proportionally more fat because the rate-limiting factor for muscle synthesis is already close to their genetic ceiling. Fun times.\r\n\r\nI wrote a helper to determine this split based on the user's self-reported experience level:\r\n\r\n```typescript\r\nexport function getGainFatFraction(liftingExperience?: LiftingExperience): number {\r\n    switch (liftingExperience) {\r\n        case 'beginner':\r\n            return 0.4; // ~60% lean gain\r\n        case 'advanced':\r\n            return 0.6; // ~40% lean gain\r\n        case 'intermediate':\r\n        default:\r\n            return 0.5; // 50/50\r\n    }\r\n}\r\n```\r\n\r\nNow, instead of blindly dividing a caloric surplus by 7700, Musclog calculates the *effective* caloric cost of gaining a kilogram of mixed body weight based on the user's experience level:\r\n\r\n```typescript\r\nexport function getEffectiveKcalPerKgGain(liftingExperience?: LiftingExperience): number {\r\n    const fatFraction = getGainFatFraction(liftingExperience);\r\n    return fatFraction * CALORIES_BUILD_KG_FAT + (1 - fatFraction) * CALORIES_BUILD_KG_MUSCLE;\r\n}\r\n```\r\n\r\nFor a beginner: `(0.4 * 8840) + (0.6 * 3900) = 5,876 kcal per kg`. That is a massive difference from the generic 7700 kcal rule, and it's exactly the kind of gap that explains why my projections were so consistently wrong.\r\n\r\n![Musclog Goals Management screen showing a cutting goal with daily calorie target, macros, target weight and body fat percentage](../../uploads/blog/2026/02/musclog-goals-management.png)\r\n\r\n*The Goals Management screen in Musclog - this is where the projection engine actually runs. The calorie targets and weight goals you set here feed directly into the model described in this post.*\r\n\r\n## The math of losing weight (the Hall-Forbes model)\r\n\r\nWeight loss requires a completely different approach, and this is where things got genuinely complicated.\r\n\r\nWhen you're in a caloric deficit, you're *liberating* stored energy rather than constructing new tissue - so the stored energy density is what matters now, not the construction cost. The problem is that you're not losing pure fat. You lose a mix of fat and lean mass, and the ratio of that mix shifts depending on how lean you already are. The leaner you are, the more lean mass you'll lose relative to fat in a deficit. This is a well-documented phenomenon and it has real implications for how you should calculate weight loss projections.\r\n\r\nTo model this accurately, I implemented the energy density equations from [Kevin Hall's 2008 paper *\"What is the required energy deficit per unit weight loss?\"*](https://pmc.ncbi.nlm.nih.gov/articles/PMC2376744/) (published in the *International Journal of Obesity*) and the Forbes curve from the book mentioned above. And to calculate the lean-to-fat loss ratio dynamically based on current body fat percentage, I had to implement the Principal branch of the Lambert W function in TypeScript to solve the underlying differential equations - a technique described in [Hall's follow-up 2010 paper *\"Predicting metabolic adaptation, body weight, and body fat changes in humans\"*](https://pmc.ncbi.nlm.nih.gov/articles/PMC2838532/) in the *American Journal of Physiology*:\r\n\r\n```typescript\r\nfunction lambertW(z: number): number {\r\n    if (z < -1 / Math.E) return NaN;\r\n    if (z === 0) return 0;\r\n    let w = z < 1 ? z : Math.log(z);\r\n    for (let i = 0; i < 30; i++) {\r\n        const ew = Math.exp(w);\r\n        const f = w * ew - z;\r\n        if (Math.abs(f) < 1e-10) return w;\r\n        const fp = ew * (w + 1);\r\n        w = w - f / fp;\r\n    }\r\n    return w;\r\n}\r\n```\r\n\r\nI want to be upfront that I did not sit down and derive this from scratch - I worked from the Hall (2008) paper and adapted the implementation for TypeScript. But I did validate it against real data from my own cutting phases (courtesy of the tracking infrastructure I [built back in 2023](/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/)), and the model holds up well in practice.\r\n\r\nUsing this, Musclog dynamically calculates exactly how many calories are in a kilogram of weight lost for a specific user, based on their current body fat percentage. If a user doesn't know their body fat, the app safely falls back to the classic ~7700 kcal/kg rule as a baseline - because some approximation beats no approximation.\r\n\r\n## This still isn't perfect\r\n\r\nI want to be honest about the limits here. These are population-level estimates. Individual variation in muscle protein synthesis efficiency, hormonal environment, training stimulus, sleep quality, and genetics means that no static model will perfectly predict what happens to any specific person's body.\r\n\r\nThe P-ratio values I'm using (0.4 / 0.5 / 0.6) are reasonable averages from the literature, not individually calibrated numbers. In a future version, I'd love to calculate a user's personal P-ratio from their historical data - Musclog already has everything it needs for that, it's just not implemented yet.\r\n\r\nBut \"more accurate than 7700 kcal/kg for everything\" is a meaningful improvement over the status quo, and the projections have been noticeably better since I shipped this.\r\n\r\n## Conclusion\r\n\r\nBuilding side projects is fun because you start off thinking \"I'll just make a simple calculator,\" and a week later you're reading 1970s physiology papers and implementing Lambert W functions in TypeScript at midnight. Classic.\r\n\r\nBy treating weight gain as an expensive construction project and weight loss as dynamic energy liberation, the projections in Musclog are vastly more accurate than the standard BMR+500 calculators floating around the web. And they actually matched my real-world data once I got it right, which is the most satisfying outcome in any side project.\r\n\r\nThis whole line of work started with the [Google Fitness API TDEE script I wrote in 2023](/en/blog/coding/using-google-fitness-api-to-calculate-my-tdee-and-more/) - if you haven't read that one and you're into this kind of obsessive fitness tracking, it's worth a look.\r\n\r\nIf you want to poke around the code, [Musclog is fully open-source on GitHub](https://github.com/blopa/musclog-app). And if you actually want to use it, you can [download it on Google Play](https://play.google.com/store/apps/details?id=com.werules.logger). Drop a comment if you want to geek out over the math.\r\n\r\nSee you in the next one!\r\n","frontmatter":{"tags":["typescript","musclog","algorithms","fitness","nutrition","bodybuilding"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2026-02-12T00:00:00.000Z","id":null,"path":"why-the-7700-calorie-rule-is-broken-and-how-i-fixed-it-in-my-app","show":true,"title":"Why the 7700 calorie rule is broken (and how I fixed it in my app)","hideExcerpt":false,"subtitle":"Spoiler: your body did not read the rulebook"}},"next":{"excerpt":"Picture this: you’re in the gym. You’re between sets. You’re…","html":"<p>Picture this: you’re in the gym. You’re between sets. You’re sweating. You have about 90 seconds before you need to grab the bar again, and you’re standing there poking at your own app like someone’s dad trying to navigate a website from 2008. Four taps to log a single set. Four taps. In an app you wrote. That you could change. And somehow, every single time, you do the four taps because there’s always something else to build first.</p>\n<p>That was Musclog. My fitness tracker. My “300 hours I’ll never get back” project that I <a href=\"/en/blog/coding/musclog-leveraging-my-reactjs-experience-to-build-a-react-native-app/\">wrote about back in 2024</a>. It worked. My friends used it. I tracked every workout and every gram of food for months.</p>\n<p>It was also, objectively, one of the ugliest apps I had ever put on a phone.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 222.265625%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Original Musclog design - the before photo nobody asked for\"\n        title=\"Original Musclog design - the before photo nobody asked for\"\n        src=\"/static/d41801c4cd3ed5901fa8893f146a8c94/42a19/musclog-workout-screenshot.png\"\n        srcset=\"/static/d41801c4cd3ed5901fa8893f146a8c94/e3135/musclog-workout-screenshot.png 256w,\n/static/d41801c4cd3ed5901fa8893f146a8c94/06341/musclog-workout-screenshot.png 512w,\n/static/d41801c4cd3ed5901fa8893f146a8c94/42a19/musclog-workout-screenshot.png 1024w,\n/static/d41801c4cd3ed5901fa8893f146a8c94/b4ad3/musclog-workout-screenshot.png 1080w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Original Musclog design - the before photo nobody asked for</figcaption>\n  </figure></p>\n<p>I knew it. My friends knew it. The one friend honest enough to say “dude, this looks like a website” knew it and said it to my face with zero hesitation. But it worked, and for a while that was enough. “It works” is the developer’s equivalent of “it’s fine” - technically true, emotionally a complete lie you’ve agreed to live with.</p>\n<p>Then the UX debt started compound-interesting its way into my life. Logging a workout felt like filing paperwork. Adding food required jumping between three different screens when it should have been one. Navigating the app in the gym, already sweating and under a time constraint, was an exercise in friction I had accidentally designed myself. I kept adding features on top of this visual chaos, which is a great way to make a hoarder’s house even harder to navigate. And not to mention the bugs, that sometimes would simply crash the app and then sit on Sentry, unsolved.</p>\n<p>Something had to change. And by “something” I mean everything.</p>\n<h2 id=\"the-designer-i-will-never-be\" style=\"position:relative;\"><a href=\"#the-designer-i-will-never-be\" aria-label=\"the designer i will never be permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The designer I will never be</h2>\n<p>The original app was built with <a href=\"https://reactnativepaper.com/\" target=\"_blank\" rel=\"noreferrer\">react-native-paper</a>, which is a perfectly decent library. The problem wasn’t react-native-paper. The problem was me. I slapped components together in whatever order made sense at the time, picked colors by vibes, and shipped without any real design system. No spacing scale. No semantic color palette. Just pure developer instinct applied directly to a UI, which, as it turns out, produces exactly the kind of app I ended up with.</p>\n<p>The same concept looked different across three screens because each screen was built at a different point in my understanding of the app. Primary buttons had different heights depending on where you found them. Cards had three different corner radii that I had presumably chosen with great intention and then completely forgotten about. Colors were hardcoded everywhere. The whole thing was held together with confidence and duct tape.</p>\n<p>The migration plan: rip out react-native-paper, replace it with <a href=\"https://www.nativewind.dev/\" target=\"_blank\" rel=\"noreferrer\">NativeWind</a> (Tailwind CSS for React Native), and build a real design system from scratch. Simple. Straightforward. Completely insane given the size of the codebase at this point.</p>\n<p>I went and did it anyway, because I have no self-preservation instinct when it comes to side projects.</p>\n<h2 id=\"enter-stitch\" style=\"position:relative;\"><a href=\"#enter-stitch\" aria-label=\"enter stitch permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Enter Stitch</h2>\n<p>I had been watching <a href=\"https://stitch.withgoogle.com/\" target=\"_blank\" rel=\"noreferrer\">Google Stitch</a> since it launched. The short version: it’s an AI tool that generates mobile app UI from prompts and screenshots. You describe the aesthetic you want, drop in reference screens, and it generates coherent React components. For a developer who has genuinely tried to learn design three separate times and failed each time for fundamentally different reasons, this felt like cheating in the best possible way.</p>\n<p>I fed it the existing Musclog screens and described what I wanted: dark theme, fitness and performance aesthetic, something that felt like looking at serious data rather than a to-do list. I wanted it to feel like the dashboard of something that means business, not like a wellness app that’s going to suggest you “take a breath” before logging your deadlift.</p>\n<p>I iterated for a full weekend and landed on what Stitch calls it the <strong>“Kinetic Depth”</strong> design system. Deep greens for the surfaces (swampGreen <code class=\"language-text\">#0a1f1a</code> for the background, charcoalGreen <code class=\"language-text\">#141a17</code> for cards, gunmetalGreen <code class=\"language-text\">#1a2420</code> for elevated elements). Indigo-to-emerald gradients for primary actions and progress indicators. Large, clear numbers for the things that actually matter: weight lifted, reps completed, calories in.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"New Musclog home screen\"\n        title=\"New Musclog home screen\"\n        src=\"/static/e506b300918526ea840241afb0a13ded/673a2/musclog-home-screen.png\"\n        srcset=\"/static/e506b300918526ea840241afb0a13ded/e3135/musclog-home-screen.png 256w,\n/static/e506b300918526ea840241afb0a13ded/06341/musclog-home-screen.png 512w,\n/static/e506b300918526ea840241afb0a13ded/673a2/musclog-home-screen.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">New Musclog home screen</figcaption>\n  </figure></p>\n<p>The unexpected part was how much this process forced me to think clearly about the product itself. To generate a good screen in Stitch, I had to articulate what that screen was <em>for</em> before I could describe what it should look like. That process exposed UX problems that had nothing to do with colors. One step in the workout logging flow existed purely because of a technical limitation I had worked around with UI instead of fixing the actual problem. <del>Fixed. Pretended it never happened.</del> Fixed, then refactored the underlying service so it could never come back.</p>\n<p><del>I</del> Stitch also wrote a proper design document for the first time in this project’s life: surface colors with names, semantic colors for macros (protein is always indigo <code class=\"language-text\">#6366f1</code>, fat is always amber <code class=\"language-text\">#f59e0b</code>, carbs are emerald <code class=\"language-text\">#10b981</code>, fiber is pink <code class=\"language-text\">#ec4899</code>), spacing scale at 4/8/12/16/20/24/32px, border radius standards at 12px for inputs and 16px for primary cards. Things I should have defined before writing a single line of UI code. The “just ship it” bill arrived, as it always does, with interest.</p>\n<h2 id=\"the-architecture-nobody-asked-about\" style=\"position:relative;\"><a href=\"#the-architecture-nobody-asked-about\" aria-label=\"the architecture nobody asked about permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The architecture nobody asked about</h2>\n<p>Since I was already burning the UI down to the studs, I took the opportunity to tighten the underlying architecture too. This is the part where most people’s eyes glaze over, but if you’re building something with local-first data storage in React Native, some of these decisions might save you time.</p>\n<p>Musclog uses <a href=\"https://watermelondb.dev/\" target=\"_blank\" rel=\"noreferrer\">WatermelonDB</a> for local storage. Not raw SQLite, not MMKV, not AsyncStorage. WatermelonDB sits on top of SQLite on native and <a href=\"https://github.com/techfort/LokiJS\" target=\"_blank\" rel=\"noreferrer\">LokiJS</a> on web, and it gives you a model layer with reactive queries. When data changes, any component observing that query re-renders automatically. No manual state sync. No “did I remember to refresh the list after saving?” bugs. The same codebase runs on Android and in the browser without touching the data layer, which matters when you want to test something quickly without spinning up a device.</p>\n<p>The layered structure goes: schema definition at the bottom, then models, then services that handle CRUD and business logic. Non-database services (AI, notifications, Health Connect sync) live separately in their own <code class=\"language-text\">services/</code> dir. Every write goes through <code class=\"language-text\">database.write(async () => { ... })</code>. No exceptions. Nested write blocks cause deadlocks in WatermelonDB, and they’re a pain to debug, so the rule is simple: never nest them, and if you find yourself wanting to, you’ve designed something wrong upstream.</p>\n<p>Here’s what that looks like in <code class=\"language-text\">NutritionCheckinService.ts</code> when creating a batch of weekly check-ins:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">static</span> <span class=\"token keyword\">async</span> <span class=\"token function\">createBatch</span><span class=\"token punctuation\">(</span>\r\n    nutritionGoalId<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">,</span>\r\n    checkins<span class=\"token operator\">:</span> NutritionCheckinInput<span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\r\n<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span>NutritionCheckin<span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token operator\">></span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">return</span> <span class=\"token keyword\">await</span> database<span class=\"token punctuation\">.</span><span class=\"token function\">write</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">async</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> collection <span class=\"token operator\">=</span> database<span class=\"token punctuation\">.</span><span class=\"token generic-function\"><span class=\"token function\">get</span><span class=\"token generic class-name\"><span class=\"token operator\">&lt;</span>NutritionCheckin<span class=\"token operator\">></span></span></span><span class=\"token punctuation\">(</span><span class=\"token string\">'nutrition_checkins'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n        <span class=\"token keyword\">const</span> preparedRecords <span class=\"token operator\">=</span> checkins<span class=\"token punctuation\">.</span><span class=\"token function\">map</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>data<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span>\r\n            collection<span class=\"token punctuation\">.</span><span class=\"token function\">prepareCreate</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>record<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\r\n                record<span class=\"token punctuation\">.</span>nutritionGoalId <span class=\"token operator\">=</span> nutritionGoalId<span class=\"token punctuation\">;</span>\r\n                record<span class=\"token punctuation\">.</span>checkinDate <span class=\"token operator\">=</span> data<span class=\"token punctuation\">.</span>checkinDate<span class=\"token punctuation\">;</span>\r\n                record<span class=\"token punctuation\">.</span>targetWeight <span class=\"token operator\">=</span> data<span class=\"token punctuation\">.</span>targetWeight<span class=\"token punctuation\">;</span>\r\n                record<span class=\"token punctuation\">.</span>status <span class=\"token operator\">=</span> data<span class=\"token punctuation\">.</span>status <span class=\"token operator\">??</span> <span class=\"token string\">'pending'</span><span class=\"token punctuation\">;</span>\r\n            <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\r\n        <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n        <span class=\"token keyword\">await</span> database<span class=\"token punctuation\">.</span><span class=\"token function\">batch</span><span class=\"token punctuation\">(</span><span class=\"token operator\">...</span>preparedRecords<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">return</span> preparedRecords<span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The <code class=\"language-text\">prepareCreate</code> + <code class=\"language-text\">database.batch()</code> combo is how WatermelonDB handles multiple inserts atomically without multiple round trips. One <code class=\"language-text\">write()</code> block, one batch, done. You never call another service’s <code class=\"language-text\">database.write()</code> from inside here, because that’s how you get a deadlock at 11pm that somehow only reproduces on device.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog workout logging screen\"\n        title=\"Musclog workout logging screen\"\n        src=\"/static/e50a22c9cedb095800c16037fcda6fee/673a2/musclog-workout-logging.png\"\n        srcset=\"/static/e50a22c9cedb095800c16037fcda6fee/e3135/musclog-workout-logging.png 256w,\n/static/e50a22c9cedb095800c16037fcda6fee/06341/musclog-workout-logging.png 512w,\n/static/e50a22c9cedb095800c16037fcda6fee/673a2/musclog-workout-logging.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog workout logging screen</figcaption>\n  </figure></p>\n<p>Sensitive data, specifically your weight history, body fat percentages, and nutrition logs, gets AES-encrypted before it hits the database. Not because I’m expecting someone to hack a local SQLite file, but because this is health data. It deserves to be treated accordingly. The <code class=\"language-text\">encryptionHelpers.ts</code> file handles encrypt/decrypt transparently through the service layer. You never think about it as a user. You never have to think about it as a contributor either, because it’s in one place and everything routes through it.</p>\n<p>So when you log a meal, here’s what actually gets stored:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// database/encryptionHelpers.ts</span>\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">encryptNutritionLogSnapshot</span><span class=\"token punctuation\">(</span>plain<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\r\n    loggedFoodName<span class=\"token operator\">?</span><span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\r\n    loggedCalories<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n    loggedProtein<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n    loggedCarbs<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n    loggedFat<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n    loggedFiber<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n    loggedMicros<span class=\"token operator\">?</span><span class=\"token operator\">:</span> Record<span class=\"token operator\">&lt;</span><span class=\"token builtin\">string</span><span class=\"token punctuation\">,</span> <span class=\"token builtin\">number</span> <span class=\"token operator\">|</span> <span class=\"token keyword\">undefined</span><span class=\"token operator\">></span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> <span class=\"token punctuation\">[</span>\r\n        loggedFoodName<span class=\"token punctuation\">,</span>\r\n        loggedCalories<span class=\"token punctuation\">,</span>\r\n        loggedProtein<span class=\"token punctuation\">,</span>\r\n        loggedCarbs<span class=\"token punctuation\">,</span>\r\n        loggedFat<span class=\"token punctuation\">,</span>\r\n        loggedFiber<span class=\"token punctuation\">,</span>\r\n        loggedMicrosJson<span class=\"token punctuation\">,</span>\r\n    <span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token builtin\">Promise</span><span class=\"token punctuation\">.</span><span class=\"token function\">all</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">[</span>\r\n        <span class=\"token function\">encryptOptionalString</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedFoodName<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token function\">encryptNumber</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedCalories<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token function\">encryptNumber</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedProtein<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token function\">encryptNumber</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedCarbs<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token function\">encryptNumber</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedFat<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token function\">encryptNumber</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedFiber<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        <span class=\"token function\">encryptJson</span><span class=\"token punctuation\">(</span>plain<span class=\"token punctuation\">.</span>loggedMicros<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n    <span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> loggedFoodName<span class=\"token punctuation\">,</span> loggedCalories<span class=\"token punctuation\">,</span> loggedProtein<span class=\"token punctuation\">,</span> loggedCarbs<span class=\"token punctuation\">,</span> loggedFat<span class=\"token punctuation\">,</span> loggedFiber<span class=\"token punctuation\">,</span> loggedMicrosJson <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>That <code class=\"language-text\">loggedCalories: 165</code> you typed in? It’s an AES-encrypted string in the database. <code class=\"language-text\">loggedFoodName: \"Chicken breast\"</code> is also encrypted. Even the micronutrients JSON blob is encrypted. Everything decrypts at read time through the service layer. The encryption key itself is derived per device and never leaves it.</p>\n<p>The chart system has its own quirk worth mentioning. Musclog uses <a href=\"https://commerce.nearform.com/open-source/victory-native/\" target=\"_blank\" rel=\"noreferrer\">Victory Native</a> for charts on mobile, which uses Skia for rendering. Skia doesn’t work on web. So every chart component has a <code class=\"language-text\">.web.tsx</code> counterpart using regular <a href=\"https://commerce.nearform.com/open-source/victory/\" target=\"_blank\" rel=\"noreferrer\">Victory</a> with SVG instead. The Expo bundler picks the right file automatically based on the extension. It sounds like double the work and it kind of is, but the alternative is charts that silently break on web, and I use the web version a lot during development.</p>\n<p>The same <code class=\"language-text\">LineChart</code> component, two runtimes:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// components/charts/LineChart.tsx - native (Skia)</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Area<span class=\"token punctuation\">,</span> CartesianChart<span class=\"token punctuation\">,</span> Line<span class=\"token punctuation\">,</span> Scatter <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'victory-native'</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">import</span> Animated<span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> useAnimatedStyle<span class=\"token punctuation\">,</span> useSharedValue <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'react-native-reanimated'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token comment\">// components/charts/LineChart.web.tsx - web (SVG)</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> VictoryArea<span class=\"token punctuation\">,</span> VictoryAxis<span class=\"token punctuation\">,</span> VictoryChart<span class=\"token punctuation\">,</span> VictoryLine<span class=\"token punctuation\">,</span> VictoryScatter <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'victory'</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Same props interface, same behavior, different rendering backends. The Expo bundler resolves <code class=\"language-text\">LineChart</code> to the <code class=\"language-text\">.web.tsx</code> file on web and the <code class=\"language-text\">.tsx</code> file everywhere else. Zero conditionals in the component that actually uses it.</p>\n<p>Volume tracking works the same way at the infrastructure level, except the interesting problem there is the math. Musclog tracks volume as estimated one-rep max, not raw weight x reps, because 5 reps at 80kg and 12 reps at 60kg are different training stimuli that produce the same number on a naive volume chart. The problem is there’s no single accepted 1RM formula. Brzycki, Epley, Lander, Mayhew - they all give you a slightly different number for the same set, and the research doesn’t pick a clear winner. So Musclog runs all seven and averages them:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// utils/workoutCalculator.ts</span>\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">calculateAverage1RM</span><span class=\"token punctuation\">(</span>weight<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">,</span> reps<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">,</span> rir<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token operator\">=</span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> formulas<span class=\"token operator\">:</span> FormulaType<span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span>\r\n        <span class=\"token string\">'Epley'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'Brzycki'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'Lander'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'Lombardi'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'Mayhew'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'OConner'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'Wathan'</span><span class=\"token punctuation\">,</span>\r\n    <span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">let</span> total1RM <span class=\"token operator\">=</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">let</span> validFormulas <span class=\"token operator\">=</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\r\n\r\n    formulas<span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>formula<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> oneRM <span class=\"token operator\">=</span> <span class=\"token function\">calculate1RM</span><span class=\"token punctuation\">(</span>weight<span class=\"token punctuation\">,</span> reps<span class=\"token punctuation\">,</span> formula<span class=\"token punctuation\">,</span> rir<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>oneRM <span class=\"token operator\">!==</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n            total1RM <span class=\"token operator\">+=</span> oneRM<span class=\"token punctuation\">;</span>\r\n            validFormulas<span class=\"token operator\">++</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token punctuation\">}</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token keyword\">return</span> validFormulas <span class=\"token operator\">></span> <span class=\"token number\">0</span> <span class=\"token operator\">?</span> total1RM <span class=\"token operator\">/</span> validFormulas <span class=\"token operator\">:</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The <code class=\"language-text\">rir</code> parameter is Reps in Reserve: if you stopped at 8 but had 2 more in the tank, <code class=\"language-text\">rir = 2</code> adjusts the estimate upward to reflect what you could actually lift. Logging your RIR is optional. The kind of person who built their own fitness app and tracks everything in a spreadsheet is usually also the kind of person who tracks their RIR. <del>I’m not saying it’s me.</del> It’s me.</p>\n<h2 id=\"wait-what-about-the-actual-food-data\" style=\"position:relative;\"><a href=\"#wait-what-about-the-actual-food-data\" aria-label=\"wait what about the actual food data permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Wait, what about the actual food data?</h2>\n<p>Right. The redesign was the visible part of this update. The more significant part, at least from a “is this app actually useful” standpoint, was completely rebuilding how food tracking works.</p>\n<p>The original version relied heavily on the nutritional information coming from Health Connect. If you didn’t have another app to do the nutrition tracking, you were left in the dumpster. That’s acceptable for a weekend project. It’s not acceptable for an app people use daily to track calories, protein, carbs, fat, and 40+ micronutrients across multiple meals.</p>\n<p>To my genuine surprise, I discovered that high-quality food data doesn’t actually live behind a corporate gatekeeper. There are massive, free public APIs - like the USDA and Open Food Facts - that provide everything from deep micronutrient breakdowns to global barcode lookups without charging a cent. Finding these was a major “aha!” moment: if the data is public and the processing happens right on your device, there is absolutely no technical justification for nutrition tracking to be a subscription-based SaaS. Most “premium” apps are essentially charging you a monthly fee to act as a middleman for data they don’t even own, but that’s a spicy discussion I’ve saved for another time.\r\nMusclog now connects to two real food databases.</p>\n<h3 id=\"usda-fooddata-central\" style=\"position:relative;\"><a href=\"#usda-fooddata-central\" aria-label=\"usda fooddata central permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>USDA FoodData Central</h3>\n<p><a href=\"https://fdc.nal.usda.gov/\" target=\"_blank\" rel=\"noreferrer\">USDA FoodData Central</a> is the US Department of Agriculture’s public nutritional database. Hundreds of thousands of foods, from branded products to raw ingredients, with detailed macro and micronutrient breakdowns. It’s government data, it’s free, and the API doesn’t require a credit card, which as you’ll see is a non-trivial consideration for me. Coverage for American and global branded products is genuinely solid. This is the backbone of the search.</p>\n<p>The USDA identifies nutrients by numeric codes, so mapping them to something human-readable requires a lookup layer. Here’s what the mapper looks like in practice:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// utils/usdaMapper.ts</span>\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">mapUSDAFoodToUnified</span><span class=\"token punctuation\">(</span>food<span class=\"token operator\">:</span> USDAFood<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> UnifiedFoodResult <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> nutrients <span class=\"token operator\">=</span> food<span class=\"token punctuation\">.</span>foodNutrients<span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token comment\">// USDA uses nutrient number codes - 1008/208 is energy, 1003/203 is protein, etc.</span>\r\n    <span class=\"token keyword\">const</span> calories <span class=\"token operator\">=</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'1008'</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">??</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'208'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> protein  <span class=\"token operator\">=</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'1003'</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">??</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'203'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> carbs    <span class=\"token operator\">=</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'1005'</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">??</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'205'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> fat      <span class=\"token operator\">=</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'1004'</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">??</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'204'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> fiber    <span class=\"token operator\">=</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'1079'</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">??</span> <span class=\"token function\">mapUSDANutritient</span><span class=\"token punctuation\">(</span>nutrients<span class=\"token punctuation\">,</span> <span class=\"token string\">'291'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\r\n        id<span class=\"token operator\">:</span> <span class=\"token function\">String</span><span class=\"token punctuation\">(</span>food<span class=\"token punctuation\">.</span>fdcId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n        name<span class=\"token operator\">:</span> food<span class=\"token punctuation\">.</span>description<span class=\"token punctuation\">,</span>\r\n        brand<span class=\"token operator\">:</span> food<span class=\"token punctuation\">.</span>brandOwner<span class=\"token punctuation\">,</span>\r\n        calories<span class=\"token operator\">:</span> calories <span class=\"token operator\">!==</span> <span class=\"token keyword\">undefined</span> <span class=\"token operator\">?</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">round</span><span class=\"token punctuation\">(</span>calories<span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> <span class=\"token keyword\">undefined</span><span class=\"token punctuation\">,</span>\r\n        protein<span class=\"token punctuation\">,</span> carbs<span class=\"token punctuation\">,</span> fat<span class=\"token punctuation\">,</span> fiber<span class=\"token punctuation\">,</span>\r\n        source<span class=\"token operator\">:</span> <span class=\"token string\">'usda'</span><span class=\"token punctuation\">,</span>\r\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The double <code class=\"language-text\">??</code> fallback exists because USDA has two different nutrient numbering schemes depending on the data type (Foundation Foods vs. Branded Foods). Both map to the same output shape.</p>\n<h3 id=\"open-food-facts\" style=\"position:relative;\"><a href=\"#open-food-facts\" aria-label=\"open food facts permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Open Food Facts</h3>\n<p><a href=\"https://world.openfoodfacts.org/\" target=\"_blank\" rel=\"noreferrer\">Open Food Facts</a> is a community-driven database of food products from around the world. Think Wikipedia but for nutrition labels: anyone can add a product, the data is open under the ODbL license, and because it’s globally crowdsourced it covers products that the USDA database mostly ignores, like whatever fermented dairy thing is in the Dutch supermarket five minutes from my apartment. When your primary nutrition database assumes you exclusively eat American products and you live in the Netherlands, you start appreciating open global datasets very quickly.</p>\n<p>Querying it is refreshingly simple for something this comprehensive:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// hooks/useUnifiedFoodSearch.ts</span>\r\n<span class=\"token keyword\">const</span> url <span class=\"token operator\">=</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">https://world.openfoodfacts.org/cgi/search.pl</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token operator\">+</span>\r\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">?search_terms=</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span><span class=\"token function\">encodeURIComponent</span><span class=\"token punctuation\">(</span>query<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token operator\">+</span>\r\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">&amp;json=1</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token operator\">+</span>\r\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">&amp;page_size=20</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token operator\">+</span>\r\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">&amp;fields=code,product_name,brands,nutriments,serving_size,image_url</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">const</span> response <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span>url<span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> signal<span class=\"token operator\">:</span> abortController<span class=\"token punctuation\">.</span>signal <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token keyword\">const</span> result <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> response<span class=\"token punctuation\">.</span><span class=\"token function\">json</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">const</span> products <span class=\"token operator\">=</span> result<span class=\"token punctuation\">.</span>products<span class=\"token punctuation\">.</span><span class=\"token function\">filter</span><span class=\"token punctuation\">(</span>\r\n    <span class=\"token punctuation\">(</span>product<span class=\"token operator\">:</span> SearchResultProduct<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token function\">getProductName</span><span class=\"token punctuation\">(</span>product<span class=\"token punctuation\">)</span>\r\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Both searches run in parallel (with abort controllers so stale requests don’t race each other) and the results merge into a single unified list. The user picks from local foods, Open Food Facts results, and USDA results all at once without knowing or caring which backend each came from.</p>\n<p>Between the two, you can search for pretty much any food and get real nutritional data without typing anything manually. And because Musclog now tracks over 40 micronutrients beyond the standard macros, you can actually see things like your magnesium intake, your zinc levels, your vitamin D. Turns out this matters once you start looking at what you’re consistently missing. For me it’s magnesium. It’s always magnesium.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog nutrition tracking with full macro breakdown\"\n        title=\"Musclog nutrition tracking with full macro breakdown\"\n        src=\"/static/db58bd04e4b3d987b097905fd5377335/673a2/musclog-nutrition-screen.png\"\n        srcset=\"/static/db58bd04e4b3d987b097905fd5377335/e3135/musclog-nutrition-screen.png 256w,\n/static/db58bd04e4b3d987b097905fd5377335/06341/musclog-nutrition-screen.png 512w,\n/static/db58bd04e4b3d987b097905fd5377335/673a2/musclog-nutrition-screen.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog nutrition tracking with full macro breakdown</figcaption>\n  </figure></p>\n<p>There’s also a barcode scanner. Point your camera at a product, it looks up the barcode in Open Food Facts, adds it to your log. I scan my greek yogurt package every morning entirely out of habit at this point. Frictionless logging is what I wanted when I started this project and I finally have it, two versions and somewhere north of 500 hours later.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAArCAIAAAD3xz8iAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKFElEQVRIxy3P+VvSCQLH8e/Pu0/NaONolh2WlqElmaiAct/3FSC3oIKWR5madyoqCuWd2jhqCeIBmkfmmRcpIKKiWTpW1rTNts0+M/PDzo+zT+3+Aa/P8/4A3WM2EOdOqc7Q1f2osqrEMti/7Fxdca051tbXNreKi7IJsZep2Bg8IgIVExYdHhQWdPTMCZ8Qxu2oOCywsOrJKdXXGKq7jMbq6jLrYN+y07nicjvdG27PVklxDhFxhYaFEpGRGNilaHDQxWD/MwHe50MviuN5wOrWy9a2Jr1B99Bk0uvLrdber3jNsfYFl93JI6MgdDyMhI7CwsHQrzjw+LfQy2ezbmgAx+ZOU0uL3qB7ZDbfNVRYrD1fscuxtu72bFWU5VMx0UxiLBkTjYu9DIs4Fxbsf9r/GyQMnJudBjg2X7S1t+sNVd1mc12tbsBiWnHaV1ZdjjX3usdTWV5AwUSziLEUTDQ+LgIWcT40yP+U/2E0HJyXm/Ylu/PhQ8PdKmNvb2N9zcCAadnpsLtcDrd7w7OlqyiiYqJZpFjq//E50Fm/E36HMLDwwtsZgGv75YNHnboabZfJWFtbZTJ1LD5fsq0sL9vtLrdbW5ZPRkfRCXAiCoKBg2PAwSGBvgF+hzDIiMK8DMD2Yss4bu3oahubGDeZ2mdnxrdf7ezs7f60v/f2/cEPrXVSDk7Gw/NpCBY+Gg0NBYecCDpxhMRCFJZnA2t7ewvLtsHBXtvK8yej/e41+4dfPn789Olfnz//9vvvA+bOa1J6qoSm4OD4ZCgOfikCdCr4xBEOHV1WmQfsvH6/ZF/pHzAt2hZHhvvsKwtv3x+8+/DhHx8/fvr1V/PDtkQeVsnDiGhwOjIcCj57IdA34Pu/sSmIipJsYNHhmpqdNJk6pmenhqxG+/L8wc/vPvzy8fOvn3/74/eno5aSrOTbmcnpGrlGKZIIORgEBAK+IOaRy4pvAp0P24dHLH0D5pXVVfvq6uKK4+X+21cHH5/aNvsn7ZYZV/+U0zRqy9U23Sisaewarmo2FdW0F+qaKwyNwPD45OjknM31Yv/9p6ezNv19Y/mD0Xrzs4ae6Y6hhf5Jx8jCxpT9Rd+0u3VwpWdq3Ty9YZza6J/bqjXPAb0jz7oHxhdWd4zWiTJtVX2HtbBlvHNwfnjWueDcdm7tOT077he79o2XzxxbY4ubI4tbY0vbPROO2nYzUGeocMyNrC9PPh0dnJxb2t593WieoaQ1BWLTL5I0AnVuVv6dCp2uokKrLb+jLSsqKcgpzL2ZplawqWjgYZlisCbZ2Z2/0le9veH49x9/LKy9RF5rC0SrFBKhXptXXZqtzUvLz1RlqUU3VNxMBTNDzkyT0RRsNDBgbJ0d6hxqKzW3VrXq88sKMk3GjjGbZ8hq7m6uaG+sajaUGkpzKvKvF2cmFFwT39YIbiVybyg5UjYKEPFIWer47BTpLY0kWcoVkKKM96t6Z9YrWvqrDLUNd6u7WvV3y/MK0uXZyfx0OTNDwVQLSYl8ooSJAPgUeJqKo1Zw5CIGlYxMTeC4nvWPPffcMhhT8qvVN4ukSenVZTlJUmaimCbl4RV8ooiN0UhoMhYCuBwSALl0BnQuIOikT1iQr4KD7Gs37OzudvQ+rm1+UNdQn5OdWVV0I0PFS5XRNSKKmIGQs1E8MlxMhwNE6HklE5qbQMrkRRXwwM3X8Uumyjc7Ls/i41lL6+OeFtODe52Nugd1lU3VRY1VBY3VxW11lcaOZhUPDSQQQbeFkHIVqkgGz+JdTqGCRDiQ0z6/6lq29rSZ2ip/aChfmhubmH5yp7Qg9VpSQ1NtU2tjt7E9SUgA0sS4vFRWuoJ4TYxRcuE8YriKBxufsI4vLA4/GRqydtXqi4aGjD0DRpVadjWeVaItLK0sbrpvUPLxwEBn/b7j6c/uyXero69tllfzffvzxtmpsYaWTvczi+VHfWGmamZswGoxJ8jjmTRieWmhXl9Zb9AmC0mApadjZ21xf2Pp9fr8u82F956Fnz3zr3e3pmemRkyNtSXXizITFqeHp2cnGxrv3W9p6Os3DQ9b7tdXcQkQ4LHV9NO282DH+dZjO/AsvXHPHKzP/vON5/X+zsSTQeOPDX3tBktX/Uhfh/nHuoct1fe0OXkZCh4JGnLKB3gy/nh3Z2N3c3nPvbDnmt1bnd5zThx4ltZnep+Y6k0tlY3aW3mpohQRWUyLpSHC48CBF8/4nvY9fNLn70CdrsBpX3i9t33wcm3P9WxrcXh7aWTXvfSTe37l8X1LfVZTofxOCvO6ABGPv0SMPBkL8o0O9o4J9g4/7QUwYGdSubDq3ITh7nr30vjexuKbjfn9tbnt50/cE10r1rrprtLR+7lGnaa1SFp5nXZLFJdIDZcTw0iQkwCdEMKnhcYjzwhRZ1M40bocRe+D6qWJgU3bmHvy0ZxR9/SHgtGmrJ4qdXuJrCKVkh0PyxLCNQokjx0BMGNPMQjBnLhANjyQGnmcCPajRB4XYEDXRPjyW8pmbUa77kZbaVJdNr80mZAnQ2YJYem8KI00NpFzBSBd8qFH+DIgxziwk+yYAD4qOIEGERHBXGQIF3lORo1Ml5BKM0T3ilLu5kpqMlnaFFKxEn1LEJPBiwFw0FBU+HHsRR9KhB8r5gQbGsCKCaBG+lOuHCNfOUYA+6IueKFAR+jQsyom7HYireamQH+TW5ZCqUilABIBnc8m0LBR2MizqFBf/CUfBsSfHRPAiDrOiDrOjD5Oi/Qngr9HgbyRF7wRoO/YsUEqRlSGBF+sYQJKKSdBzJLFM+K5JCYpFg8LxYQfw188Qrj0HTXyKDP6GB3yxZMj/Ajh38eFeMeFeCEueOHD/VjwIECTKFKrhEmKq0oJWx7PEPNpXAaOjLyCunwKCfoOF+ZNjjjKgQbQo45RvnpsmA861Ccm6DA8+DCgUUs0SSKNKj5ZKUiU85RillxIlwhoAg6JToCjIefiQL7IkG+woV6UCD/alaPky76EcB906BH4uW8AtUaeopGlqqWapC8JyQn8LxMStkxAE7AJXAaWTozDQsPgYf7w89/Cgw6hLnjhL3pjQr0RIV6ASilMSBCqVCJNskSTJP7fi8QEvlopHLT0TE+OpSSKeEwcm4YmIa/Aw09HBXnDgg6hQg6jQN7AVTaBx2NIpQKNRq5RyzRJ4tRkiVLG1etK/vzzP3/99deDljoODSXikXlMHIuCIqIgcRHBUcE+kMBDgETM4bApPBZBLuEmJ0nUapk6SZyoFGZmJNkdtlcvt29nXxfxqRIhXcglXWXiODQ0gxRHREfHQUCAXHZVoYhXyHhiAV0koCXIr6qTJWq1VKmK16ilKWqJQsJWynhyCUcazxTzqUIukcfAsqgoBhkJUClYKhVPpxMYdCKNgiVi4WQiks2msNkUJoPIoOOZDAKTjmfQcDQKlkpGk4kIEi4Wh4aiYiP/C25erkGcvDzmAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog barcode scanner in action\"\n        title=\"Musclog barcode scanner in action\"\n        src=\"/static/6ba0390a71c49bf2c85af708d46f3e67/673a2/musclog-barcode-scanner.png\"\n        srcset=\"/static/6ba0390a71c49bf2c85af708d46f3e67/e3135/musclog-barcode-scanner.png 256w,\n/static/6ba0390a71c49bf2c85af708d46f3e67/06341/musclog-barcode-scanner.png 512w,\n/static/6ba0390a71c49bf2c85af708d46f3e67/673a2/musclog-barcode-scanner.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog barcode scanner in action</figcaption>\n  </figure></p>\n<p>For when you can’t scan anything because you’re eating at a restaurant or staring at a plate of “it’s probably chicken with some sauce,” there’s OCR label scanning (tesseract.js on web, rn-mlkit-ocr on native) and AI-based photo estimation, which I’ll get to in a second.</p>\n<p>The OCR follows the same dual-file pattern as the charts. Identical function signature, different engine, bundler picks the right one at build time:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// utils/ocr.ts - native (Google ML Kit, runs on-device)</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> recognizeText <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'rn-mlkit-ocr'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">performOcr</span><span class=\"token punctuation\">(</span>imageUri<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span><span class=\"token builtin\">string</span> <span class=\"token operator\">|</span> <span class=\"token keyword\">null</span><span class=\"token operator\">></span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> result <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">recognizeText</span><span class=\"token punctuation\">(</span>imageUri<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> text <span class=\"token operator\">=</span> result<span class=\"token punctuation\">.</span>text<span class=\"token punctuation\">.</span><span class=\"token function\">trim</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">return</span> text<span class=\"token punctuation\">.</span>length <span class=\"token operator\">></span> <span class=\"token number\">0</span> <span class=\"token operator\">?</span> text <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span>\r\n\r\n<span class=\"token comment\">// utils/ocr.web.ts - web (Tesseract.js, runs entirely in the browser)</span>\r\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> createWorker <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'tesseract.js'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">const</span> <span class=\"token constant\">OCR_LANGS</span> <span class=\"token operator\">=</span> <span class=\"token string\">'eng+spa+por+nld+deu+fra'</span><span class=\"token punctuation\">;</span>\r\n\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">performOcr</span><span class=\"token punctuation\">(</span>imageUri<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span><span class=\"token builtin\">string</span> <span class=\"token operator\">|</span> <span class=\"token keyword\">null</span><span class=\"token operator\">></span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> worker <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">createWorker</span><span class=\"token punctuation\">(</span><span class=\"token constant\">OCR_LANGS</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> data<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> text <span class=\"token punctuation\">}</span> <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> worker<span class=\"token punctuation\">.</span><span class=\"token function\">recognize</span><span class=\"token punctuation\">(</span>imageUri<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">await</span> worker<span class=\"token punctuation\">.</span><span class=\"token function\">terminate</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">return</span> text<span class=\"token punctuation\">.</span><span class=\"token function\">trim</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>ML Kit runs on-device and is fast. Tesseract.js boots and kills a full worker for every scan, which is not elegant, but it works offline and nothing leaves the browser. The language pack covers English, Spanish, Portuguese, Dutch, German, and French. I live in the Netherlands, I’m Brazilian, and I was not shipping a grocery label scanner that choked on Dutch cheese packaging.</p>\n<h2 id=\"the-ai-coach-grew-up\" style=\"position:relative;\"><a href=\"#the-ai-coach-grew-up\" aria-label=\"the ai coach grew up permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The AI coach grew up</h2>\n<p>The original chatbot was called Chad. Yeah I know, not very creative, but I was focussing more on “shipping” features than making them appealing. And that’s ok, right, Chad?</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 52.34375%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAArklEQVQoz2P4TwFgwCXx+/fPj+9fffn8kSTN//7////z589TB9ed3Dbh1K45X798AIn++0dYM0TR6xfPD+3YdGbv0odHp717eRciQazmxw/vbV058fTuRR8eXfn9+yexzoba/PLxhiXt6xZPqyovW7p8OQkBBtF/8uTJlrb2DZs23r179/fv36Rp/vDhw40bNz5//vz371+So+rHjx/Pnz9/9uzZr1+/SNZMTCIBADuEQuzgh5oVAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"yes.\"\n        title=\"yes.\"\n        src=\"/static/0786b7e77bff75acc8234ca8a6cd3f35/42a19/chad-yes.png\"\n        srcset=\"/static/0786b7e77bff75acc8234ca8a6cd3f35/e3135/chad-yes.png 256w,\n/static/0786b7e77bff75acc8234ca8a6cd3f35/06341/chad-yes.png 512w,\n/static/0786b7e77bff75acc8234ca8a6cd3f35/42a19/chad-yes.png 1024w,\n/static/0786b7e77bff75acc8234ca8a6cd3f35/e8464/chad-yes.png 1536w,\n/static/0786b7e77bff75acc8234ca8a6cd3f35/e71fd/chad-yes.png 1910w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">yes.</figcaption>\n  </figure></p>\n<p>It’s now called Loggy, it supports both <a href=\"https://openai.com/\" target=\"_blank\" rel=\"noreferrer\">OpenAI</a> and <a href=\"https://deepmind.google/technologies/gemini/\" target=\"_blank\" rel=\"noreferrer\">Google Gemini</a>, and it’s actually connected to your data in a meaningful way. When you ask Loggy something, it knows who you are: your recent workouts, your nutrition logs, your weight trends, your current goals. “Was my training volume last week on track?” gets a real answer based on real data, not a generic tip about progressive overload you’ve already read fifteen times.</p>\n<p>The structured output layer was one of the more interesting technical problems here. Getting LLMs to reliably output structured data (instead of prose that looks like structured data but breaks your JSON parser) requires care. The <code class=\"language-text\">makeSchemaStrict</code> utility in <code class=\"language-text\">utils/coachAI.ts</code> takes any JSON schema and enforces <code class=\"language-text\">additionalProperties: false</code> on every nested object while marking all fields as <code class=\"language-text\">required</code>. This goes into the OpenAI function calling config and tells the model “return exactly this shape or fail cleanly.” It makes the difference between a feature that works 95% of the time and one that actually works.</p>\n<p>The function itself is simple enough that I almost didn’t write it down:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// utils/coachAI.ts</span>\r\n<span class=\"token keyword\">function</span> <span class=\"token function\">makeSchemaStrict</span><span class=\"token punctuation\">(</span>schema<span class=\"token operator\">:</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">any</span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>schema<span class=\"token punctuation\">.</span>type <span class=\"token operator\">===</span> <span class=\"token string\">'object'</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> properties <span class=\"token operator\">=</span> schema<span class=\"token punctuation\">.</span>properties <span class=\"token operator\">?</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>schema<span class=\"token punctuation\">.</span>properties <span class=\"token punctuation\">}</span> <span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n\r\n        Object<span class=\"token punctuation\">.</span><span class=\"token function\">keys</span><span class=\"token punctuation\">(</span>properties<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>key<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\r\n            properties<span class=\"token punctuation\">[</span>key<span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token function\">makeSchemaStrict</span><span class=\"token punctuation\">(</span>properties<span class=\"token punctuation\">[</span>key<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n        <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\r\n            <span class=\"token operator\">...</span>schema<span class=\"token punctuation\">,</span>\r\n            properties<span class=\"token punctuation\">,</span>\r\n            additionalProperties<span class=\"token operator\">:</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">,</span> <span class=\"token comment\">// OpenAI requires this for strict mode</span>\r\n        <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>schema<span class=\"token punctuation\">.</span>type <span class=\"token operator\">===</span> <span class=\"token string\">'array'</span> <span class=\"token operator\">&amp;&amp;</span> schema<span class=\"token punctuation\">.</span>items<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>schema<span class=\"token punctuation\">,</span> items<span class=\"token operator\">:</span> <span class=\"token function\">makeSchemaStrict</span><span class=\"token punctuation\">(</span>schema<span class=\"token punctuation\">.</span>items<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token keyword\">return</span> schema<span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Recursively walks every nested object in the schema and slaps <code class=\"language-text\">additionalProperties: false</code> on it. Without this, OpenAI’s strict function calling rejects the schema entirely. With it, the model is constrained to exactly the shape you defined. No surprise extra keys. No fields that exist in one response but not another. The AI proposes, the schema enforces.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog AI coach chat\"\n        title=\"Musclog AI coach chat\"\n        src=\"/static/960530dbeeeeb62bafee9d8e8401f88d/673a2/musclog-ai-coach.png\"\n        srcset=\"/static/960530dbeeeeb62bafee9d8e8401f88d/e3135/musclog-ai-coach.png 256w,\n/static/960530dbeeeeb62bafee9d8e8401f88d/06341/musclog-ai-coach.png 512w,\n/static/960530dbeeeeb62bafee9d8e8401f88d/673a2/musclog-ai-coach.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog AI coach chat</figcaption>\n  </figure></p>\n<p>The photo analysis feature is the one that makes new users do a double-take. Take a photo of your meal, Loggy estimates portion sizes and nutritional content. It’s not replacing a food scale for precision tracking, but for eating out or for days when you genuinely can’t scan anything, it gets you in the right ballpark fast. The flow sends the image to the model with a structured schema for the response, extracts the estimates, and then makes you confirm before logging. That last step matters: the AI proposes, you decide.</p>\n<p>System prompts live in <code class=\"language-text\">utils/prompts.ts</code> and pull in custom instructions from the <code class=\"language-text\">ai_custom_prompts</code> table, so the AI behavior is configurable without touching code.</p>\n<h2 id=\"weekly-check-ins-the-feature-im-most-proud-of\" style=\"position:relative;\"><a href=\"#weekly-check-ins-the-feature-im-most-proud-of\" aria-label=\"weekly check ins the feature im most proud of permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Weekly check-ins: the feature I’m most proud of</h2>\n<p>Every week, Musclog runs an automated analysis of your 7-day rolling averages for weight, caloric intake, and activity. You get a status: On Track, Ahead, or Behind. If your numbers are diverging from your targets, it can recalculate your nutrition goals based on what actually happened rather than locking you into a plan that clearly isn’t matching reality.</p>\n<p>The core of it is <code class=\"language-text\">getCheckinMetrics</code>, which takes a check-in record and pulls all the data from the 7-day window ending on that date:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// database/services/NutritionCheckinService.ts</span>\r\n<span class=\"token keyword\">static</span> <span class=\"token keyword\">async</span> <span class=\"token function\">getCheckinMetrics</span><span class=\"token punctuation\">(</span>checkin<span class=\"token operator\">:</span> NutritionCheckin<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span>CheckinMetrics<span class=\"token operator\">></span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> periodEnd <span class=\"token operator\">=</span> checkin<span class=\"token punctuation\">.</span>checkinDate<span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">const</span> periodStart <span class=\"token operator\">=</span> periodEnd <span class=\"token operator\">-</span> <span class=\"token number\">7</span> <span class=\"token operator\">*</span> <span class=\"token number\">24</span> <span class=\"token operator\">*</span> <span class=\"token number\">60</span> <span class=\"token operator\">*</span> <span class=\"token number\">60</span> <span class=\"token operator\">*</span> <span class=\"token number\">1000</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token comment\">// Pull all weight measurements for the 7-day window</span>\r\n    <span class=\"token keyword\">const</span> weightMetrics <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> database\r\n        <span class=\"token punctuation\">.</span><span class=\"token generic-function\"><span class=\"token function\">get</span><span class=\"token generic class-name\"><span class=\"token operator\">&lt;</span>UserMetric<span class=\"token operator\">></span></span></span><span class=\"token punctuation\">(</span><span class=\"token string\">'user_metrics'</span><span class=\"token punctuation\">)</span>\r\n        <span class=\"token punctuation\">.</span><span class=\"token function\">query</span><span class=\"token punctuation\">(</span>\r\n            <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">where</span><span class=\"token punctuation\">(</span><span class=\"token string\">'type'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'weight'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n            <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">where</span><span class=\"token punctuation\">(</span><span class=\"token string\">'date'</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">between</span><span class=\"token punctuation\">(</span>periodStart<span class=\"token punctuation\">,</span> periodEnd<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n            <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">where</span><span class=\"token punctuation\">(</span><span class=\"token string\">'deleted_at'</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">eq</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">null</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\r\n            <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">sortBy</span><span class=\"token punctuation\">(</span><span class=\"token string\">'date'</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span>asc<span class=\"token punctuation\">)</span>\r\n        <span class=\"token punctuation\">)</span>\r\n        <span class=\"token punctuation\">.</span><span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token comment\">// Decrypt each value (weights are AES-encrypted in the DB)</span>\r\n    <span class=\"token keyword\">const</span> decryptedWeights<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">for</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">const</span> metric <span class=\"token keyword\">of</span> weightMetrics<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> value <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> metric<span class=\"token punctuation\">.</span><span class=\"token function\">getDecrypted</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        decryptedWeights<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span>value<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token keyword\">const</span> avgWeight <span class=\"token operator\">=</span> decryptedWeights<span class=\"token punctuation\">.</span>length <span class=\"token operator\">></span> <span class=\"token number\">0</span>\r\n        <span class=\"token operator\">?</span> decryptedWeights<span class=\"token punctuation\">.</span><span class=\"token function\">reduce</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>a<span class=\"token punctuation\">,</span> b<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> a <span class=\"token operator\">+</span> b<span class=\"token punctuation\">,</span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">/</span> decryptedWeights<span class=\"token punctuation\">.</span>length\r\n        <span class=\"token operator\">:</span> checkin<span class=\"token punctuation\">.</span>targetWeight<span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token comment\">// How far is the actual average from where we expected to be?</span>\r\n    <span class=\"token keyword\">const</span> trend <span class=\"token operator\">=</span> avgWeight <span class=\"token operator\">-</span> checkin<span class=\"token punctuation\">.</span>targetWeight<span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token comment\">// Nutrition: group logs by day, calculate average calories and consistency</span>\r\n    <span class=\"token keyword\">const</span> nutritionLogs <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> database\r\n        <span class=\"token punctuation\">.</span><span class=\"token generic-function\"><span class=\"token function\">get</span><span class=\"token generic class-name\"><span class=\"token operator\">&lt;</span>NutritionLog<span class=\"token operator\">></span></span></span><span class=\"token punctuation\">(</span><span class=\"token string\">'nutrition_logs'</span><span class=\"token punctuation\">)</span>\r\n        <span class=\"token punctuation\">.</span><span class=\"token function\">query</span><span class=\"token punctuation\">(</span><span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">where</span><span class=\"token punctuation\">(</span><span class=\"token string\">'date'</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">between</span><span class=\"token punctuation\">(</span>periodStart<span class=\"token punctuation\">,</span> periodEnd<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">where</span><span class=\"token punctuation\">(</span><span class=\"token string\">'deleted_at'</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">Q</span><span class=\"token punctuation\">.</span><span class=\"token function\">eq</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">null</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\r\n        <span class=\"token punctuation\">.</span><span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token keyword\">const</span> caloriesByDay <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Map<span class=\"token operator\">&lt;</span><span class=\"token builtin\">number</span><span class=\"token punctuation\">,</span> <span class=\"token builtin\">number</span><span class=\"token operator\">></span></span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token keyword\">for</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">const</span> log <span class=\"token keyword\">of</span> nutritionLogs<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> snapshot <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> log<span class=\"token punctuation\">.</span><span class=\"token function\">getDecryptedSnapshot</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">const</span> dayKey <span class=\"token operator\">=</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">floor</span><span class=\"token punctuation\">(</span>log<span class=\"token punctuation\">.</span>date <span class=\"token operator\">/</span> <span class=\"token punctuation\">(</span><span class=\"token number\">24</span> <span class=\"token operator\">*</span> <span class=\"token number\">60</span> <span class=\"token operator\">*</span> <span class=\"token number\">60</span> <span class=\"token operator\">*</span> <span class=\"token number\">1000</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n        caloriesByDay<span class=\"token punctuation\">.</span><span class=\"token function\">set</span><span class=\"token punctuation\">(</span>dayKey<span class=\"token punctuation\">,</span> <span class=\"token punctuation\">(</span>caloriesByDay<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span>dayKey<span class=\"token punctuation\">)</span> <span class=\"token operator\">??</span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">+</span> snapshot<span class=\"token punctuation\">.</span>loggedCalories<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token keyword\">const</span> consistency <span class=\"token operator\">=</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">round</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>caloriesByDay<span class=\"token punctuation\">.</span>size <span class=\"token operator\">/</span> <span class=\"token number\">7</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">*</span> <span class=\"token number\">100</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// % of days with logs</span>\r\n    <span class=\"token comment\">// ...and so on for workouts, body fat, active minutes</span>\r\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The <code class=\"language-text\">trend</code> is the key number: positive means you’re heavier than the target predicted, negative means you’re ahead. Combined with <code class=\"language-text\">consistency</code> (what percentage of the 7 days you actually logged food), the service can infer whether the variance is real or just missing data.</p>\n<p>The TDEE calculation is empirical rather than formula-based. Musclog looks at your actual weight change over time combined with your actual logged calorie intake and works backwards to estimate your real maintenance calories. If you’ve been eating 2,200 calories a day and losing 0.3kg per week, the math tells you something about your actual metabolism that the Harris-Benedict formula never will, especially if your activity level doesn’t fit neatly into “sedentary” or “moderately active” or whatever vague category you picked during onboarding.</p>\n<p>The function that does this lives in <code class=\"language-text\">utils/nutritionCalculator.ts</code>. The logic is straightforward; the constants are not:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// utils/nutritionCalculator.ts</span>\r\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> calculateTDEE <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>params<span class=\"token operator\">:</span> TDEEParams<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\r\n    <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> totalCalories<span class=\"token punctuation\">,</span> totalDays<span class=\"token punctuation\">,</span> initialWeight<span class=\"token punctuation\">,</span> finalWeight<span class=\"token punctuation\">,</span>\r\n            initialFatPercentage<span class=\"token punctuation\">,</span> finalFatPercentage<span class=\"token punctuation\">,</span> bmr<span class=\"token punctuation\">,</span> activityLevel <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> params<span class=\"token punctuation\">;</span>\r\n\r\n    <span class=\"token comment\">// Path 1: real tracking data exists - derive TDEE from the First Law of Thermodynamics</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>totalDays <span class=\"token operator\">&amp;&amp;</span> totalCalories <span class=\"token operator\">&amp;&amp;</span> initialWeight <span class=\"token operator\">&amp;&amp;</span> finalWeight<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">const</span> weightDifference <span class=\"token operator\">=</span> finalWeight <span class=\"token operator\">-</span> initialWeight<span class=\"token punctuation\">;</span>\r\n\r\n        <span class=\"token comment\">// Split weight change into fat vs lean mass.</span>\r\n        <span class=\"token comment\">// Exact when we have body fat % on both ends; Hall/Forbes curve estimate otherwise.</span>\r\n        <span class=\"token keyword\">let</span> fatDifference<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n        <span class=\"token keyword\">let</span> leanDifference<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\r\n\r\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>initialFatPercentage <span class=\"token operator\">!==</span> <span class=\"token keyword\">undefined</span> <span class=\"token operator\">&amp;&amp;</span> finalFatPercentage <span class=\"token operator\">!==</span> <span class=\"token keyword\">undefined</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n            <span class=\"token keyword\">const</span> initialFatMass <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>initialFatPercentage <span class=\"token operator\">*</span> initialWeight<span class=\"token punctuation\">)</span> <span class=\"token operator\">/</span> <span class=\"token number\">100</span><span class=\"token punctuation\">;</span>\r\n            <span class=\"token keyword\">const</span> finalFatMass <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>finalFatPercentage <span class=\"token operator\">*</span> finalWeight<span class=\"token punctuation\">)</span> <span class=\"token operator\">/</span> <span class=\"token number\">100</span><span class=\"token punctuation\">;</span>\r\n            fatDifference <span class=\"token operator\">=</span> finalFatMass <span class=\"token operator\">-</span> initialFatMass<span class=\"token punctuation\">;</span>\r\n            leanDifference <span class=\"token operator\">=</span> weightDifference <span class=\"token operator\">-</span> fatDifference<span class=\"token punctuation\">;</span>\r\n        <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\r\n            <span class=\"token keyword\">const</span> comp <span class=\"token operator\">=</span> <span class=\"token function\">getWeightChangeComposition</span><span class=\"token punctuation\">(</span>initialWeight <span class=\"token operator\">*</span> <span class=\"token number\">0.25</span><span class=\"token punctuation\">,</span> weightDifference<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n            fatDifference <span class=\"token operator\">=</span> comp<span class=\"token punctuation\">.</span>fatChangeKg<span class=\"token punctuation\">;</span>\r\n            leanDifference <span class=\"token operator\">=</span> comp<span class=\"token punctuation\">.</span>leanChangeKg<span class=\"token punctuation\">;</span>\r\n        <span class=\"token punctuation\">}</span>\r\n\r\n        <span class=\"token comment\">// Building vs burning fat and muscle have different thermodynamic costs.</span>\r\n        <span class=\"token comment\">// These are not interchangeable constants - they're separate measured values.</span>\r\n        <span class=\"token keyword\">const</span> leanCalories <span class=\"token operator\">=</span> leanDifference <span class=\"token operator\">></span> <span class=\"token number\">0</span>\r\n            <span class=\"token operator\">?</span> leanDifference <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_BUILD_KG_MUSCLE</span>   <span class=\"token comment\">// 3900 kcal/kg to build</span>\r\n            <span class=\"token operator\">:</span> leanDifference <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_STORED_KG_MUSCLE</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// 1250 kcal/kg stored</span>\r\n\r\n        <span class=\"token keyword\">const</span> fatCalories <span class=\"token operator\">=</span> fatDifference <span class=\"token operator\">></span> <span class=\"token number\">0</span>\r\n            <span class=\"token operator\">?</span> fatDifference <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_BUILD_KG_FAT</span>   <span class=\"token comment\">// 8840 kcal/kg to build</span>\r\n            <span class=\"token operator\">:</span> fatDifference <span class=\"token operator\">*</span> <span class=\"token constant\">CALORIES_STORED_KG_FAT</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// 7730 kcal/kg stored</span>\r\n\r\n        <span class=\"token comment\">// TDEE = (energy consumed − energy locked in tissue changes) / days</span>\r\n        <span class=\"token keyword\">return</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">round</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>totalCalories <span class=\"token operator\">-</span> <span class=\"token punctuation\">(</span>fatCalories <span class=\"token operator\">+</span> leanCalories<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">/</span> totalDays<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token comment\">// Path 2: no history yet - fall back to BMR x activity multiplier</span>\r\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>bmr <span class=\"token operator\">&amp;&amp;</span> activityLevel<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\r\n        <span class=\"token keyword\">return</span> Math<span class=\"token punctuation\">.</span><span class=\"token function\">round</span><span class=\"token punctuation\">(</span>bmr <span class=\"token operator\">*</span> <span class=\"token constant\">ACTIVITY_MULTIPLIERS</span><span class=\"token punctuation\">[</span>activityLevel<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\r\n    <span class=\"token punctuation\">}</span>\r\n\r\n    <span class=\"token keyword\">return</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\r\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>The asymmetry between building fat (8840 kcal/kg) and burning it (7730 kcal/kg) is real. Thermodynamic efficiency isn’t 100%, so creating new tissue costs more than what ends up stored. Same principle applies to muscle. If you just hardcode 7700 and move on you get something that’s approximately right, but you’re averaging away the exact signal you built the whole check-in system to find.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog weekly check-in screen\"\n        title=\"Musclog weekly check-in screen\"\n        src=\"/static/985f3c03966e36135d3a2b761be56160/673a2/musclog-weekly-checkin.png\"\n        srcset=\"/static/985f3c03966e36135d3a2b761be56160/e3135/musclog-weekly-checkin.png 256w,\n/static/985f3c03966e36135d3a2b761be56160/06341/musclog-weekly-checkin.png 512w,\n/static/985f3c03966e36135d3a2b761be56160/673a2/musclog-weekly-checkin.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog weekly check-in screen</figcaption>\n  </figure></p>\n<p>This is not a flashy feature. You don’t notice it until the end of the week. But having an app that notices “you’ve been under your calorie target all week and your weight still hasn’t moved, let’s figure out why” is exactly the kind of insight I was building toward when I started this project. I just didn’t know it at the time. I thought I was building a workout logger.</p>\n<h2 id=\"the-other-stuff-i-quietly-shipped\" style=\"position:relative;\"><a href=\"#the-other-stuff-i-quietly-shipped\" aria-label=\"the other stuff i quietly shipped permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The other stuff I quietly shipped</h2>\n<p>Home screen widgets for quick logging and daily summaries. A menstrual cycle tracker with workout intensity recommendations by phase, because assuming a male-only user base is <del>a reasonable starting point</del> not ok so Musclog do not use gender to infer menstrual cycle data. Health Connect integration for syncing weight, nutrition, and exercise data with other Android health apps. Full data export as encrypted (or unencrypted) JSON. Import from JSON for moving between devices without losing your history.</p>\n<p>The export feature sounds boring until your phone dies, and you don’t have it. At that point you’re simultaneously grateful you built it and slightly furious at past-you for not testing the import path more thoroughly. Ask me how I know.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 537px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 216.796875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,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'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Musclog micronutrient tracking detail\"\n        title=\"Musclog micronutrient tracking detail\"\n        src=\"/static/f0ca76a952bf9a9fa74fb16c082e76ac/673a2/musclog-micronutrients.png\"\n        srcset=\"/static/f0ca76a952bf9a9fa74fb16c082e76ac/e3135/musclog-micronutrients.png 256w,\n/static/f0ca76a952bf9a9fa74fb16c082e76ac/06341/musclog-micronutrients.png 512w,\n/static/f0ca76a952bf9a9fa74fb16c082e76ac/673a2/musclog-micronutrients.png 537w\"\n        sizes=\"(max-width: 537px) 100vw, 537px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">Musclog micronutrient tracking detail</figcaption>\n  </figure></p>\n<h2 id=\"why-your-fitness-app-subscription-is-someone-elses-problem\" style=\"position:relative;\"><a href=\"#why-your-fitness-app-subscription-is-someone-elses-problem\" aria-label=\"why your fitness app subscription is someone elses problem permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Why your fitness app subscription is someone else’s problem</h2>\n<p>I want to say something about the fitness app market, because I have been holding this in for roughly two years and this is my blog.</p>\n<p>The story repeats on a schedule you can set your watch to: app launches free, gets users, introduces a premium tier, gradually migrates core features behind the paywall, raises prices, users complain on Reddit, nothing changes. I’ve watched this happen to apps I genuinely liked using. MyFitnessPal went from actually good and free to a subscription where setting your own macro targets costs money. Other apps introduced “premium food databases” that took away the useful thing and sold it back monthly. One app I used for a while moved the progress charts behind a paywall. The progress charts. In a fitness tracking app. The one feature that shows whether the app is even working.</p>\n<blockquote>\n<p>“But without subscription revenue, how do you keep the lights on?”\r\nThe lights are a charging cable and cost zero per month. Next question.</p>\n</blockquote>\n<p>I understand the economics. Servers cost money. Engineering teams cost money. Building a sustainable product business is genuinely hard and someone has to pay for it. Fine.</p>\n<p>But Musclog doesn’t have servers. There’s no cloud infrastructure. No database I’m paying to host somewhere. Your data lives on your phone. The food databases I use are public and free. The AI features, if you use them, talk directly to OpenAI or Google using your own API key: you pay them, no middleman, no cut. The app is free on <a href=\"https://play.google.com/store/apps/details?id=com.werules.logger\" target=\"_blank\" rel=\"noreferrer\">Google Play</a> and open source on <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">GitHub</a>.</p>\n<p><figure class=\"gatsby-resp-image-figure\" style=\"\">\n    <span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 54.6875%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVQozwGfAmD9ALmplcO7rNDJupqSg7m2rc3OyeHe1PLw6OTi2+Hf2PPw5erXwqSagoKAcbmgkdqzmtCqkp2Ti4F9eEI7MgC6rpq8t62ZkYyJhYCIgX25trLu6+DZ2NXJysvS1NS8ureRgXWPgG55bmCEdWeQhHmbkIOdlIuGeW2HdWMAuKyaubamdIZ0ZW5iZnRjyMa1+O/e4NnN3NbN1dPMjoR9hVxJnHdgbVpJloh2uaybkIN5i4N5gnlrTUM5AI+Ie2FpWTx2VT5yUj16V4GPdePSvf734+bayK6mnKKUi9+ojd2qiodvWsCynp+Me4lgTI5tWZ6WhlFKQgB3c2djbFxMdV1Zg2lNemGCknvx4c3z7NzTw7S/raKdmY+EZ1Kni2x3e2jGtaGQe27BkHiodFi5o45hW1MAdnBkam9jPmlTUXpmQnFbd4dy7+HM0LijuZJ/wrKpZnpmRGpMeI1sb4xnv7ahb2BZj2VQbkg3c1I9WE5CAJyKd2puZUBgUmB8b05uYWd3aNbIstfHtPjWw8fDtjFSOlNpV3aCdFp4Wp6iiYiAeJyaiJmShJebhGpjUwC5pZBlbGEtSEAwTUYpR0E5TEOxppH28uj9+veQnIgyTzo7WEJggltGaUp5f1+fi3h5emi+pI6cpYeEZlAAvbivc3tyHTg0GDU1Ei8wL0E/u7eu4NzU7eXhiqGNXXliM1c9UntNZoRiaH5me4N7gYZ+uKSTv7KhtaGYAJKPlH18fiY1Ni46Pi87PVBWWpiVmo2LkIuJj36Dd3Z8cTNKRDpUQWNyZ5uCc2Jtc1dtfIiNkbWelJGTmQBYX21eZHNCTVRXW19WWV1WWl1vbndubXVnaneTfHmUe3ROW21DTFVYXGl7bXBaZHFVZ3RyfIZ9cXNra3Tw91qmRwxnZwAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"The fitness app subscription cycle, illustrated\"\n        title=\"The fitness app subscription cycle, illustrated\"\n        src=\"/static/5fcabb7cf250c7994bfc3908d114b28c/42a19/fitness-app-subscription-meme.png\"\n        srcset=\"/static/5fcabb7cf250c7994bfc3908d114b28c/e3135/fitness-app-subscription-meme.png 256w,\n/static/5fcabb7cf250c7994bfc3908d114b28c/06341/fitness-app-subscription-meme.png 512w,\n/static/5fcabb7cf250c7994bfc3908d114b28c/42a19/fitness-app-subscription-meme.png 1024w,\n/static/5fcabb7cf250c7994bfc3908d114b28c/e8464/fitness-app-subscription-meme.png 1536w,\n/static/5fcabb7cf250c7994bfc3908d114b28c/2eb59/fitness-app-subscription-meme.png 2048w,\n/static/5fcabb7cf250c7994bfc3908d114b28c/b4639/fitness-app-subscription-meme.png 2724w\"\n        sizes=\"(max-width: 1024px) 100vw, 1024px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n    </span>\n    <figcaption class=\"gatsby-resp-image-figcaption\">The fitness app subscription cycle, illustrated</figcaption>\n  </figure></p>\n<p>This isn’t a principled stand against capitalism. It’s a design decision that makes sense for what this actually is: a personal tool I built for myself and then shared with other people. The architecture I chose naturally creates no server costs, which means I have no financial pressure to monetize, which means users have no reason to be suspicious about what I’m doing with their data. Because I’m not doing anything with their data. It’s on their phone.</p>\n<p>The offline-first design is deliberate for the same reason. Weight history, body composition, what you eat, your menstrual cycle, your workout history: none of this should live on a stranger’s server by default. Most fitness apps don’t explain clearly what they do with that data because a transparent answer would be unpopular. Musclog keeps everything local. The only data that ever leaves your device is what you explicitly send to the AI, and only when you choose to use that feature.</p>\n<p>There’s a whole population of people training in gyms with patchy WiFi, in basements, in garages, in parks, who have never once had Musclog fail because it needed to call home. That matters to me more than a monthly recurring revenue number.</p>\n<h2 id=\"whats-next\" style=\"position:relative;\"><a href=\"#whats-next\" aria-label=\"whats next permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What’s next</h2>\n<p>Onboarding is the weakest part of the app right now. Musclog is significantly more useful the more data it has, but the new user experience is still too close to “here’s the entire app, figure it out.” I want to build a proper onboarding flow that gets someone set up with their goals, their first workout template, and their first food log in under five minutes. You shouldn’t need 500 hours of context to get value from day one.</p>\n<p>BLE device support for smart scales is also on the list. Manually entering weight and body fat every day is fine. Having the scale send it to the app automatically when you step on it is better, and the infrastructure isn’t that complex. It’s mostly a matter of getting my hands on hardware to test against, which is a very solvable problem and definitely something I’ll do as soon as I finish the seventeen other things I’ve already started.</p>\n<h2 id=\"will-it-be-free-forever\" style=\"position:relative;\"><a href=\"#will-it-be-free-forever\" aria-label=\"will it be free forever permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Will it be free forever?</h2>\n<p>Fair question. The short answer is: I have no idea. The longer answer is more interesting.</p>\n<p>The code is open source under Attribution-NonCommercial-NoDerivatives 4.0 International. You can read it, fork it for personal use, learn from it. You just can’t ship a product built on it without talking to me first, because Musclog is a proprietary brand that I own, and because the five hundred hours of work behind it represent something north of €25000 in European software engineering salaries. I’m not running a charity. I’m running a side project that hasn’t asked you for money yet.</p>\n<p>If that ever changes, here’s my promise: I won’t do the subscription thing. If Musclog ever costs money, it’ll cost money once. You pay the price of a coffee, the app is yours, forever. No recurring fees. No “your premium plan has been paused.” The good old model, the one the App Store basically killed.</p>\n<p>The thing pushing me closest to that scenario right now is iOS. My girlfriend wants it on iPhone. Some close friends want it on iPhone. I’ve been hearing “just put it on the App Store” for months as if it’s a trivial thing. The Apple developer program costs 100 USD a year. Per year. Not once. Every year. Apple built a SaaS out of the right to distribute software, and I say this as someone who thinks Google Play’s one-time 35 USD fee is how it should work everywhere. So yes, if Musclog ever lands on iOS, it’ll probably have a price tag, because I am not paying a 100 USD annual toll to Apple out of goodwill.</p>\n<p>For now: Android, free, no plans to change that. So grab it while it’s free, because once you download it on Google Play, it’s yours forever. Even if you didn’t pay for it. Especially if you didn’t pay for it.</p>\n<h2 id=\"conclusion\" style=\"position:relative;\"><a href=\"#conclusion\" aria-label=\"conclusion permalink\" class=\"post-headers-link before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conclusion</h2>\n<p>Two years, two versions, 500 hours. Musclog finally looks like something I don’t mentally apologize for before showing to someone. The redesign cleared years of “just ship it” UI debt, and doing it properly forced me to think about the product in ways I should have been thinking about since the beginning. Turns out design systems exist for good reasons and not just to give designers something to talk about in Figma.</p>\n<p>It’s free. It’s open source. It doesn’t have your data. It works in airplane mode. Real food databases. AI that actually knows who you are. Automated weekly check-ins. And a dark green color scheme I’m genuinely proud of, even if an AI tool did most of the visual heavy lifting.</p>\n<p>If you want to try it: <a href=\"https://play.google.com/store/apps/details?id=com.werules.logger\" target=\"_blank\" rel=\"noreferrer\">Google Play</a>.</p>\n<p>If you want to see how it’s built or contribute something: <a href=\"https://github.com/blopa/musclog-app\" target=\"_blank\" rel=\"noreferrer\">github.com/blopa/musclog-app</a>.</p>\n<p>Lift, Log, Repeat.</p>\n<p>See you in the next one!</p>","fields":{"postHashId":"Y29kaW5ndHJ1ZW51bGwyMDI2LTAzLTIyVDAwOjAwOjAwLjAwMFo=","slug":"/2026/2026-03-22-musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam.en/","path":"/blog/coding/musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam/","locale":"en"},"rawMarkdownBody":"\r\nPicture this: you're in the gym. You're between sets. You're sweating. You have about 90 seconds before you need to grab the bar again, and you're standing there poking at your own app like someone's dad trying to navigate a website from 2008. Four taps to log a single set. Four taps. In an app you wrote. That you could change. And somehow, every single time, you do the four taps because there's always something else to build first.\r\n\r\nThat was Musclog. My fitness tracker. My \"300 hours I'll never get back\" project that I [wrote about back in 2024](/en/blog/coding/musclog-leveraging-my-reactjs-experience-to-build-a-react-native-app/). It worked. My friends used it. I tracked every workout and every gram of food for months.\r\n\r\nIt was also, objectively, one of the ugliest apps I had ever put on a phone.\r\n\r\n![Original Musclog design - the before photo nobody asked for](../../uploads/blog/2024/09/musclog-workout-screenshot.png)\r\n\r\nI knew it. My friends knew it. The one friend honest enough to say \"dude, this looks like a website\" knew it and said it to my face with zero hesitation. But it worked, and for a while that was enough. \"It works\" is the developer's equivalent of \"it's fine\" - technically true, emotionally a complete lie you've agreed to live with.\r\n\r\nThen the UX debt started compound-interesting its way into my life. Logging a workout felt like filing paperwork. Adding food required jumping between three different screens when it should have been one. Navigating the app in the gym, already sweating and under a time constraint, was an exercise in friction I had accidentally designed myself. I kept adding features on top of this visual chaos, which is a great way to make a hoarder's house even harder to navigate. And not to mention the bugs, that sometimes would simply crash the app and then sit on Sentry, unsolved.\r\n\r\nSomething had to change. And by \"something\" I mean everything.\r\n\r\n## The designer I will never be\r\n\r\nThe original app was built with [react-native-paper](https://reactnativepaper.com/), which is a perfectly decent library. The problem wasn't react-native-paper. The problem was me. I slapped components together in whatever order made sense at the time, picked colors by vibes, and shipped without any real design system. No spacing scale. No semantic color palette. Just pure developer instinct applied directly to a UI, which, as it turns out, produces exactly the kind of app I ended up with.\r\n\r\nThe same concept looked different across three screens because each screen was built at a different point in my understanding of the app. Primary buttons had different heights depending on where you found them. Cards had three different corner radii that I had presumably chosen with great intention and then completely forgotten about. Colors were hardcoded everywhere. The whole thing was held together with confidence and duct tape.\r\n\r\nThe migration plan: rip out react-native-paper, replace it with [NativeWind](https://www.nativewind.dev/) (Tailwind CSS for React Native), and build a real design system from scratch. Simple. Straightforward. Completely insane given the size of the codebase at this point.\r\n\r\nI went and did it anyway, because I have no self-preservation instinct when it comes to side projects.\r\n\r\n## Enter Stitch\r\n\r\nI had been watching [Google Stitch](https://stitch.withgoogle.com/) since it launched. The short version: it's an AI tool that generates mobile app UI from prompts and screenshots. You describe the aesthetic you want, drop in reference screens, and it generates coherent React components. For a developer who has genuinely tried to learn design three separate times and failed each time for fundamentally different reasons, this felt like cheating in the best possible way.\r\n\r\nI fed it the existing Musclog screens and described what I wanted: dark theme, fitness and performance aesthetic, something that felt like looking at serious data rather than a to-do list. I wanted it to feel like the dashboard of something that means business, not like a wellness app that's going to suggest you \"take a breath\" before logging your deadlift.\r\n\r\nI iterated for a full weekend and landed on what Stitch calls it the **\"Kinetic Depth\"** design system. Deep greens for the surfaces (swampGreen `#0a1f1a` for the background, charcoalGreen `#141a17` for cards, gunmetalGreen `#1a2420` for elevated elements). Indigo-to-emerald gradients for primary actions and progress indicators. Large, clear numbers for the things that actually matter: weight lifted, reps completed, calories in.\r\n\r\n![New Musclog home screen](../../uploads/blog/2026/03/musclog-home-screen.png)\r\n\r\nThe unexpected part was how much this process forced me to think clearly about the product itself. To generate a good screen in Stitch, I had to articulate what that screen was *for* before I could describe what it should look like. That process exposed UX problems that had nothing to do with colors. One step in the workout logging flow existed purely because of a technical limitation I had worked around with UI instead of fixing the actual problem. ~~Fixed. Pretended it never happened.~~ Fixed, then refactored the underlying service so it could never come back.\r\n\r\n~~I~~ Stitch also wrote a proper design document for the first time in this project's life: surface colors with names, semantic colors for macros (protein is always indigo `#6366f1`, fat is always amber `#f59e0b`, carbs are emerald `#10b981`, fiber is pink `#ec4899`), spacing scale at 4/8/12/16/20/24/32px, border radius standards at 12px for inputs and 16px for primary cards. Things I should have defined before writing a single line of UI code. The \"just ship it\" bill arrived, as it always does, with interest.\r\n\r\n## The architecture nobody asked about\r\n\r\nSince I was already burning the UI down to the studs, I took the opportunity to tighten the underlying architecture too. This is the part where most people's eyes glaze over, but if you're building something with local-first data storage in React Native, some of these decisions might save you time.\r\n\r\nMusclog uses [WatermelonDB](https://watermelondb.dev/) for local storage. Not raw SQLite, not MMKV, not AsyncStorage. WatermelonDB sits on top of SQLite on native and [LokiJS](https://github.com/techfort/LokiJS) on web, and it gives you a model layer with reactive queries. When data changes, any component observing that query re-renders automatically. No manual state sync. No \"did I remember to refresh the list after saving?\" bugs. The same codebase runs on Android and in the browser without touching the data layer, which matters when you want to test something quickly without spinning up a device.\r\n\r\nThe layered structure goes: schema definition at the bottom, then models, then services that handle CRUD and business logic. Non-database services (AI, notifications, Health Connect sync) live separately in their own `services/` dir. Every write goes through `database.write(async () => { ... })`. No exceptions. Nested write blocks cause deadlocks in WatermelonDB, and they're a pain to debug, so the rule is simple: never nest them, and if you find yourself wanting to, you've designed something wrong upstream.\r\n\r\nHere's what that looks like in `NutritionCheckinService.ts` when creating a batch of weekly check-ins:\r\n\r\n```typescript\r\nstatic async createBatch(\r\n    nutritionGoalId: string,\r\n    checkins: NutritionCheckinInput[]\r\n): Promise<NutritionCheckin[]> {\r\n    return await database.write(async () => {\r\n        const collection = database.get<NutritionCheckin>('nutrition_checkins');\r\n\r\n        const preparedRecords = checkins.map((data) =>\r\n            collection.prepareCreate((record) => {\r\n                record.nutritionGoalId = nutritionGoalId;\r\n                record.checkinDate = data.checkinDate;\r\n                record.targetWeight = data.targetWeight;\r\n                record.status = data.status ?? 'pending';\r\n            })\r\n        );\r\n\r\n        await database.batch(...preparedRecords);\r\n        return preparedRecords;\r\n    });\r\n}\r\n```\r\n\r\nThe `prepareCreate` + `database.batch()` combo is how WatermelonDB handles multiple inserts atomically without multiple round trips. One `write()` block, one batch, done. You never call another service's `database.write()` from inside here, because that's how you get a deadlock at 11pm that somehow only reproduces on device.\r\n\r\n![Musclog workout logging screen](../../uploads/blog/2026/03/musclog-workout-logging.png)\r\n\r\nSensitive data, specifically your weight history, body fat percentages, and nutrition logs, gets AES-encrypted before it hits the database. Not because I'm expecting someone to hack a local SQLite file, but because this is health data. It deserves to be treated accordingly. The `encryptionHelpers.ts` file handles encrypt/decrypt transparently through the service layer. You never think about it as a user. You never have to think about it as a contributor either, because it's in one place and everything routes through it.\r\n\r\nSo when you log a meal, here's what actually gets stored:\r\n\r\n```typescript\r\n// database/encryptionHelpers.ts\r\nexport async function encryptNutritionLogSnapshot(plain: {\r\n    loggedFoodName?: string;\r\n    loggedCalories: number;\r\n    loggedProtein: number;\r\n    loggedCarbs: number;\r\n    loggedFat: number;\r\n    loggedFiber: number;\r\n    loggedMicros?: Record<string, number | undefined>;\r\n}) {\r\n    const [\r\n        loggedFoodName,\r\n        loggedCalories,\r\n        loggedProtein,\r\n        loggedCarbs,\r\n        loggedFat,\r\n        loggedFiber,\r\n        loggedMicrosJson,\r\n    ] = await Promise.all([\r\n        encryptOptionalString(plain.loggedFoodName),\r\n        encryptNumber(plain.loggedCalories),\r\n        encryptNumber(plain.loggedProtein),\r\n        encryptNumber(plain.loggedCarbs),\r\n        encryptNumber(plain.loggedFat),\r\n        encryptNumber(plain.loggedFiber),\r\n        encryptJson(plain.loggedMicros),\r\n    ]);\r\n\r\n    return { loggedFoodName, loggedCalories, loggedProtein, loggedCarbs, loggedFat, loggedFiber, loggedMicrosJson };\r\n}\r\n```\r\n\r\nThat `loggedCalories: 165` you typed in? It's an AES-encrypted string in the database. `loggedFoodName: \"Chicken breast\"` is also encrypted. Even the micronutrients JSON blob is encrypted. Everything decrypts at read time through the service layer. The encryption key itself is derived per device and never leaves it.\r\n\r\nThe chart system has its own quirk worth mentioning. Musclog uses [Victory Native](https://commerce.nearform.com/open-source/victory-native/) for charts on mobile, which uses Skia for rendering. Skia doesn't work on web. So every chart component has a `.web.tsx` counterpart using regular [Victory](https://commerce.nearform.com/open-source/victory/) with SVG instead. The Expo bundler picks the right file automatically based on the extension. It sounds like double the work and it kind of is, but the alternative is charts that silently break on web, and I use the web version a lot during development.\r\n\r\nThe same `LineChart` component, two runtimes:\r\n\r\n```typescript\r\n// components/charts/LineChart.tsx - native (Skia)\r\nimport { Area, CartesianChart, Line, Scatter } from 'victory-native';\r\nimport Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';\r\n\r\n// components/charts/LineChart.web.tsx - web (SVG)\r\nimport { VictoryArea, VictoryAxis, VictoryChart, VictoryLine, VictoryScatter } from 'victory';\r\n```\r\n\r\nSame props interface, same behavior, different rendering backends. The Expo bundler resolves `LineChart` to the `.web.tsx` file on web and the `.tsx` file everywhere else. Zero conditionals in the component that actually uses it.\r\n\r\nVolume tracking works the same way at the infrastructure level, except the interesting problem there is the math. Musclog tracks volume as estimated one-rep max, not raw weight x reps, because 5 reps at 80kg and 12 reps at 60kg are different training stimuli that produce the same number on a naive volume chart. The problem is there's no single accepted 1RM formula. Brzycki, Epley, Lander, Mayhew - they all give you a slightly different number for the same set, and the research doesn't pick a clear winner. So Musclog runs all seven and averages them:\r\n\r\n```typescript\r\n// utils/workoutCalculator.ts\r\nexport function calculateAverage1RM(weight: number, reps: number, rir: number = 0): number {\r\n    const formulas: FormulaType[] = [\r\n        'Epley', 'Brzycki', 'Lander', 'Lombardi', 'Mayhew', 'OConner', 'Wathan',\r\n    ];\r\n    let total1RM = 0;\r\n    let validFormulas = 0;\r\n\r\n    formulas.forEach((formula) => {\r\n        const oneRM = calculate1RM(weight, reps, formula, rir);\r\n        if (oneRM !== null) {\r\n            total1RM += oneRM;\r\n            validFormulas++;\r\n        }\r\n    });\r\n\r\n    return validFormulas > 0 ? total1RM / validFormulas : 0;\r\n}\r\n```\r\n\r\nThe `rir` parameter is Reps in Reserve: if you stopped at 8 but had 2 more in the tank, `rir = 2` adjusts the estimate upward to reflect what you could actually lift. Logging your RIR is optional. The kind of person who built their own fitness app and tracks everything in a spreadsheet is usually also the kind of person who tracks their RIR. ~~I'm not saying it's me.~~ It's me.\r\n\r\n## Wait, what about the actual food data?\r\n\r\nRight. The redesign was the visible part of this update. The more significant part, at least from a \"is this app actually useful\" standpoint, was completely rebuilding how food tracking works.\r\n\r\nThe original version relied heavily on the nutritional information coming from Health Connect. If you didn't have another app to do the nutrition tracking, you were left in the dumpster. That's acceptable for a weekend project. It's not acceptable for an app people use daily to track calories, protein, carbs, fat, and 40+ micronutrients across multiple meals.\r\n\r\nTo my genuine surprise, I discovered that high-quality food data doesn't actually live behind a corporate gatekeeper. There are massive, free public APIs - like the USDA and Open Food Facts - that provide everything from deep micronutrient breakdowns to global barcode lookups without charging a cent. Finding these was a major \"aha!\" moment: if the data is public and the processing happens right on your device, there is absolutely no technical justification for nutrition tracking to be a subscription-based SaaS. Most \"premium\" apps are essentially charging you a monthly fee to act as a middleman for data they don't even own, but that’s a spicy discussion I’ve saved for another time.\r\nMusclog now connects to two real food databases.\r\n\r\n### USDA FoodData Central\r\n\r\n[USDA FoodData Central](https://fdc.nal.usda.gov/) is the US Department of Agriculture's public nutritional database. Hundreds of thousands of foods, from branded products to raw ingredients, with detailed macro and micronutrient breakdowns. It's government data, it's free, and the API doesn't require a credit card, which as you'll see is a non-trivial consideration for me. Coverage for American and global branded products is genuinely solid. This is the backbone of the search.\r\n\r\nThe USDA identifies nutrients by numeric codes, so mapping them to something human-readable requires a lookup layer. Here's what the mapper looks like in practice:\r\n\r\n```typescript\r\n// utils/usdaMapper.ts\r\nexport function mapUSDAFoodToUnified(food: USDAFood): UnifiedFoodResult {\r\n    const nutrients = food.foodNutrients;\r\n\r\n    // USDA uses nutrient number codes - 1008/208 is energy, 1003/203 is protein, etc.\r\n    const calories = mapUSDANutritient(nutrients, '1008') ?? mapUSDANutritient(nutrients, '208');\r\n    const protein  = mapUSDANutritient(nutrients, '1003') ?? mapUSDANutritient(nutrients, '203');\r\n    const carbs    = mapUSDANutritient(nutrients, '1005') ?? mapUSDANutritient(nutrients, '205');\r\n    const fat      = mapUSDANutritient(nutrients, '1004') ?? mapUSDANutritient(nutrients, '204');\r\n    const fiber    = mapUSDANutritient(nutrients, '1079') ?? mapUSDANutritient(nutrients, '291');\r\n\r\n    return {\r\n        id: String(food.fdcId),\r\n        name: food.description,\r\n        brand: food.brandOwner,\r\n        calories: calories !== undefined ? Math.round(calories) : undefined,\r\n        protein, carbs, fat, fiber,\r\n        source: 'usda',\r\n    };\r\n}\r\n```\r\n\r\nThe double `??` fallback exists because USDA has two different nutrient numbering schemes depending on the data type (Foundation Foods vs. Branded Foods). Both map to the same output shape.\r\n\r\n### Open Food Facts\r\n\r\n[Open Food Facts](https://world.openfoodfacts.org/) is a community-driven database of food products from around the world. Think Wikipedia but for nutrition labels: anyone can add a product, the data is open under the ODbL license, and because it's globally crowdsourced it covers products that the USDA database mostly ignores, like whatever fermented dairy thing is in the Dutch supermarket five minutes from my apartment. When your primary nutrition database assumes you exclusively eat American products and you live in the Netherlands, you start appreciating open global datasets very quickly.\r\n\r\nQuerying it is refreshingly simple for something this comprehensive:\r\n\r\n```typescript\r\n// hooks/useUnifiedFoodSearch.ts\r\nconst url = `https://world.openfoodfacts.org/cgi/search.pl` +\r\n    `?search_terms=${encodeURIComponent(query)}` +\r\n    `&json=1` +\r\n    `&page_size=20` +\r\n    `&fields=code,product_name,brands,nutriments,serving_size,image_url`;\r\n\r\nconst response = await fetch(url, { signal: abortController.signal });\r\nconst result = await response.json();\r\n\r\nconst products = result.products.filter(\r\n    (product: SearchResultProduct) => getProductName(product)\r\n);\r\n```\r\n\r\nBoth searches run in parallel (with abort controllers so stale requests don't race each other) and the results merge into a single unified list. The user picks from local foods, Open Food Facts results, and USDA results all at once without knowing or caring which backend each came from.\r\n\r\nBetween the two, you can search for pretty much any food and get real nutritional data without typing anything manually. And because Musclog now tracks over 40 micronutrients beyond the standard macros, you can actually see things like your magnesium intake, your zinc levels, your vitamin D. Turns out this matters once you start looking at what you're consistently missing. For me it's magnesium. It's always magnesium.\r\n\r\n![Musclog nutrition tracking with full macro breakdown](../../uploads/blog/2026/03/musclog-nutrition-screen.png)\r\n\r\nThere's also a barcode scanner. Point your camera at a product, it looks up the barcode in Open Food Facts, adds it to your log. I scan my greek yogurt package every morning entirely out of habit at this point. Frictionless logging is what I wanted when I started this project and I finally have it, two versions and somewhere north of 500 hours later.\r\n\r\n![Musclog barcode scanner in action](../../uploads/blog/2026/03/musclog-barcode-scanner.png)\r\n\r\nFor when you can't scan anything because you're eating at a restaurant or staring at a plate of \"it's probably chicken with some sauce,\" there's OCR label scanning (tesseract.js on web, rn-mlkit-ocr on native) and AI-based photo estimation, which I'll get to in a second.\r\n\r\nThe OCR follows the same dual-file pattern as the charts. Identical function signature, different engine, bundler picks the right one at build time:\r\n\r\n```typescript\r\n// utils/ocr.ts - native (Google ML Kit, runs on-device)\r\nimport { recognizeText } from 'rn-mlkit-ocr';\r\n\r\nexport async function performOcr(imageUri: string): Promise<string | null> {\r\n    const result = await recognizeText(imageUri);\r\n    const text = result.text.trim();\r\n    return text.length > 0 ? text : null;\r\n}\r\n\r\n// utils/ocr.web.ts - web (Tesseract.js, runs entirely in the browser)\r\nimport { createWorker } from 'tesseract.js';\r\n\r\nconst OCR_LANGS = 'eng+spa+por+nld+deu+fra';\r\n\r\nexport async function performOcr(imageUri: string): Promise<string | null> {\r\n    const worker = await createWorker(OCR_LANGS);\r\n    const { data: { text } } = await worker.recognize(imageUri);\r\n    await worker.terminate();\r\n    return text.trim() || null;\r\n}\r\n```\r\n\r\nML Kit runs on-device and is fast. Tesseract.js boots and kills a full worker for every scan, which is not elegant, but it works offline and nothing leaves the browser. The language pack covers English, Spanish, Portuguese, Dutch, German, and French. I live in the Netherlands, I'm Brazilian, and I was not shipping a grocery label scanner that choked on Dutch cheese packaging.\r\n\r\n## The AI coach grew up\r\n\r\nThe original chatbot was called Chad. Yeah I know, not very creative, but I was focussing more on \"shipping\" features than making them appealing. And that's ok, right, Chad?\r\n\r\n![yes.](../../uploads/blog/2026/03/chad-yes.png)\r\n\r\nIt's now called Loggy, it supports both [OpenAI](https://openai.com/) and [Google Gemini](https://deepmind.google/technologies/gemini/), and it's actually connected to your data in a meaningful way. When you ask Loggy something, it knows who you are: your recent workouts, your nutrition logs, your weight trends, your current goals. \"Was my training volume last week on track?\" gets a real answer based on real data, not a generic tip about progressive overload you've already read fifteen times.\r\n\r\nThe structured output layer was one of the more interesting technical problems here. Getting LLMs to reliably output structured data (instead of prose that looks like structured data but breaks your JSON parser) requires care. The `makeSchemaStrict` utility in `utils/coachAI.ts` takes any JSON schema and enforces `additionalProperties: false` on every nested object while marking all fields as `required`. This goes into the OpenAI function calling config and tells the model \"return exactly this shape or fail cleanly.\" It makes the difference between a feature that works 95% of the time and one that actually works.\r\n\r\nThe function itself is simple enough that I almost didn't write it down:\r\n\r\n```typescript\r\n// utils/coachAI.ts\r\nfunction makeSchemaStrict(schema: any): any {\r\n    if (schema.type === 'object') {\r\n        const properties = schema.properties ? { ...schema.properties } : {};\r\n\r\n        Object.keys(properties).forEach((key) => {\r\n            properties[key] = makeSchemaStrict(properties[key]);\r\n        });\r\n\r\n        return {\r\n            ...schema,\r\n            properties,\r\n            additionalProperties: false, // OpenAI requires this for strict mode\r\n        };\r\n    }\r\n\r\n    if (schema.type === 'array' && schema.items) {\r\n        return { ...schema, items: makeSchemaStrict(schema.items) };\r\n    }\r\n\r\n    return schema;\r\n}\r\n```\r\n\r\nRecursively walks every nested object in the schema and slaps `additionalProperties: false` on it. Without this, OpenAI's strict function calling rejects the schema entirely. With it, the model is constrained to exactly the shape you defined. No surprise extra keys. No fields that exist in one response but not another. The AI proposes, the schema enforces.\r\n\r\n![Musclog AI coach chat](../../uploads/blog/2026/03/musclog-ai-coach.png)\r\n\r\nThe photo analysis feature is the one that makes new users do a double-take. Take a photo of your meal, Loggy estimates portion sizes and nutritional content. It's not replacing a food scale for precision tracking, but for eating out or for days when you genuinely can't scan anything, it gets you in the right ballpark fast. The flow sends the image to the model with a structured schema for the response, extracts the estimates, and then makes you confirm before logging. That last step matters: the AI proposes, you decide.\r\n\r\nSystem prompts live in `utils/prompts.ts` and pull in custom instructions from the `ai_custom_prompts` table, so the AI behavior is configurable without touching code.\r\n\r\n## Weekly check-ins: the feature I'm most proud of\r\n\r\nEvery week, Musclog runs an automated analysis of your 7-day rolling averages for weight, caloric intake, and activity. You get a status: On Track, Ahead, or Behind. If your numbers are diverging from your targets, it can recalculate your nutrition goals based on what actually happened rather than locking you into a plan that clearly isn't matching reality.\r\n\r\nThe core of it is `getCheckinMetrics`, which takes a check-in record and pulls all the data from the 7-day window ending on that date:\r\n\r\n```typescript\r\n// database/services/NutritionCheckinService.ts\r\nstatic async getCheckinMetrics(checkin: NutritionCheckin): Promise<CheckinMetrics> {\r\n    const periodEnd = checkin.checkinDate;\r\n    const periodStart = periodEnd - 7 * 24 * 60 * 60 * 1000;\r\n\r\n    // Pull all weight measurements for the 7-day window\r\n    const weightMetrics = await database\r\n        .get<UserMetric>('user_metrics')\r\n        .query(\r\n            Q.where('type', 'weight'),\r\n            Q.where('date', Q.between(periodStart, periodEnd)),\r\n            Q.where('deleted_at', Q.eq(null)),\r\n            Q.sortBy('date', Q.asc)\r\n        )\r\n        .fetch();\r\n\r\n    // Decrypt each value (weights are AES-encrypted in the DB)\r\n    const decryptedWeights: number[] = [];\r\n    for (const metric of weightMetrics) {\r\n        const { value } = await metric.getDecrypted();\r\n        decryptedWeights.push(value);\r\n    }\r\n\r\n    const avgWeight = decryptedWeights.length > 0\r\n        ? decryptedWeights.reduce((a, b) => a + b, 0) / decryptedWeights.length\r\n        : checkin.targetWeight;\r\n\r\n    // How far is the actual average from where we expected to be?\r\n    const trend = avgWeight - checkin.targetWeight;\r\n\r\n    // Nutrition: group logs by day, calculate average calories and consistency\r\n    const nutritionLogs = await database\r\n        .get<NutritionLog>('nutrition_logs')\r\n        .query(Q.where('date', Q.between(periodStart, periodEnd)), Q.where('deleted_at', Q.eq(null)))\r\n        .fetch();\r\n\r\n    const caloriesByDay = new Map<number, number>();\r\n    for (const log of nutritionLogs) {\r\n        const snapshot = await log.getDecryptedSnapshot();\r\n        const dayKey = Math.floor(log.date / (24 * 60 * 60 * 1000));\r\n        caloriesByDay.set(dayKey, (caloriesByDay.get(dayKey) ?? 0) + snapshot.loggedCalories);\r\n    }\r\n\r\n    const consistency = Math.round((caloriesByDay.size / 7) * 100); // % of days with logs\r\n    // ...and so on for workouts, body fat, active minutes\r\n}\r\n```\r\n\r\nThe `trend` is the key number: positive means you're heavier than the target predicted, negative means you're ahead. Combined with `consistency` (what percentage of the 7 days you actually logged food), the service can infer whether the variance is real or just missing data.\r\n\r\nThe TDEE calculation is empirical rather than formula-based. Musclog looks at your actual weight change over time combined with your actual logged calorie intake and works backwards to estimate your real maintenance calories. If you've been eating 2,200 calories a day and losing 0.3kg per week, the math tells you something about your actual metabolism that the Harris-Benedict formula never will, especially if your activity level doesn't fit neatly into \"sedentary\" or \"moderately active\" or whatever vague category you picked during onboarding.\r\n\r\nThe function that does this lives in `utils/nutritionCalculator.ts`. The logic is straightforward; the constants are not:\r\n\r\n```typescript\r\n// utils/nutritionCalculator.ts\r\nexport const calculateTDEE = (params: TDEEParams): number => {\r\n    const { totalCalories, totalDays, initialWeight, finalWeight,\r\n            initialFatPercentage, finalFatPercentage, bmr, activityLevel } = params;\r\n\r\n    // Path 1: real tracking data exists - derive TDEE from the First Law of Thermodynamics\r\n    if (totalDays && totalCalories && initialWeight && finalWeight) {\r\n        const weightDifference = finalWeight - initialWeight;\r\n\r\n        // Split weight change into fat vs lean mass.\r\n        // Exact when we have body fat % on both ends; Hall/Forbes curve estimate otherwise.\r\n        let fatDifference: number;\r\n        let leanDifference: number;\r\n\r\n        if (initialFatPercentage !== undefined && finalFatPercentage !== undefined) {\r\n            const initialFatMass = (initialFatPercentage * initialWeight) / 100;\r\n            const finalFatMass = (finalFatPercentage * finalWeight) / 100;\r\n            fatDifference = finalFatMass - initialFatMass;\r\n            leanDifference = weightDifference - fatDifference;\r\n        } else {\r\n            const comp = getWeightChangeComposition(initialWeight * 0.25, weightDifference);\r\n            fatDifference = comp.fatChangeKg;\r\n            leanDifference = comp.leanChangeKg;\r\n        }\r\n\r\n        // Building vs burning fat and muscle have different thermodynamic costs.\r\n        // These are not interchangeable constants - they're separate measured values.\r\n        const leanCalories = leanDifference > 0\r\n            ? leanDifference * CALORIES_BUILD_KG_MUSCLE   // 3900 kcal/kg to build\r\n            : leanDifference * CALORIES_STORED_KG_MUSCLE; // 1250 kcal/kg stored\r\n\r\n        const fatCalories = fatDifference > 0\r\n            ? fatDifference * CALORIES_BUILD_KG_FAT   // 8840 kcal/kg to build\r\n            : fatDifference * CALORIES_STORED_KG_FAT; // 7730 kcal/kg stored\r\n\r\n        // TDEE = (energy consumed − energy locked in tissue changes) / days\r\n        return Math.round((totalCalories - (fatCalories + leanCalories)) / totalDays);\r\n    }\r\n\r\n    // Path 2: no history yet - fall back to BMR x activity multiplier\r\n    if (bmr && activityLevel) {\r\n        return Math.round(bmr * ACTIVITY_MULTIPLIERS[activityLevel]);\r\n    }\r\n\r\n    return 0;\r\n};\r\n```\r\n\r\nThe asymmetry between building fat (8840 kcal/kg) and burning it (7730 kcal/kg) is real. Thermodynamic efficiency isn't 100%, so creating new tissue costs more than what ends up stored. Same principle applies to muscle. If you just hardcode 7700 and move on you get something that's approximately right, but you're averaging away the exact signal you built the whole check-in system to find.\r\n\r\n![Musclog weekly check-in screen](../../uploads/blog/2026/03/musclog-weekly-checkin.png)\r\n\r\nThis is not a flashy feature. You don't notice it until the end of the week. But having an app that notices \"you've been under your calorie target all week and your weight still hasn't moved, let's figure out why\" is exactly the kind of insight I was building toward when I started this project. I just didn't know it at the time. I thought I was building a workout logger.\r\n\r\n## The other stuff I quietly shipped\r\n\r\nHome screen widgets for quick logging and daily summaries. A menstrual cycle tracker with workout intensity recommendations by phase, because assuming a male-only user base is ~~a reasonable starting point~~ not ok so Musclog do not use gender to infer menstrual cycle data. Health Connect integration for syncing weight, nutrition, and exercise data with other Android health apps. Full data export as encrypted (or unencrypted) JSON. Import from JSON for moving between devices without losing your history.\r\n\r\nThe export feature sounds boring until your phone dies, and you don't have it. At that point you're simultaneously grateful you built it and slightly furious at past-you for not testing the import path more thoroughly. Ask me how I know.\r\n\r\n![Musclog micronutrient tracking detail](../../uploads/blog/2026/03/musclog-micronutrients.png)\r\n\r\n## Why your fitness app subscription is someone else's problem\r\n\r\nI want to say something about the fitness app market, because I have been holding this in for roughly two years and this is my blog.\r\n\r\nThe story repeats on a schedule you can set your watch to: app launches free, gets users, introduces a premium tier, gradually migrates core features behind the paywall, raises prices, users complain on Reddit, nothing changes. I've watched this happen to apps I genuinely liked using. MyFitnessPal went from actually good and free to a subscription where setting your own macro targets costs money. Other apps introduced \"premium food databases\" that took away the useful thing and sold it back monthly. One app I used for a while moved the progress charts behind a paywall. The progress charts. In a fitness tracking app. The one feature that shows whether the app is even working.\r\n\r\n> \"But without subscription revenue, how do you keep the lights on?\"\r\n> The lights are a charging cable and cost zero per month. Next question.\r\n\r\nI understand the economics. Servers cost money. Engineering teams cost money. Building a sustainable product business is genuinely hard and someone has to pay for it. Fine.\r\n\r\nBut Musclog doesn't have servers. There's no cloud infrastructure. No database I'm paying to host somewhere. Your data lives on your phone. The food databases I use are public and free. The AI features, if you use them, talk directly to OpenAI or Google using your own API key: you pay them, no middleman, no cut. The app is free on [Google Play](https://play.google.com/store/apps/details?id=com.werules.logger) and open source on [GitHub](https://github.com/blopa/musclog-app).\r\n\r\n![The fitness app subscription cycle, illustrated](../../uploads/blog/2026/03/fitness-app-subscription-meme.png)\r\n\r\nThis isn't a principled stand against capitalism. It's a design decision that makes sense for what this actually is: a personal tool I built for myself and then shared with other people. The architecture I chose naturally creates no server costs, which means I have no financial pressure to monetize, which means users have no reason to be suspicious about what I'm doing with their data. Because I'm not doing anything with their data. It's on their phone.\r\n\r\nThe offline-first design is deliberate for the same reason. Weight history, body composition, what you eat, your menstrual cycle, your workout history: none of this should live on a stranger's server by default. Most fitness apps don't explain clearly what they do with that data because a transparent answer would be unpopular. Musclog keeps everything local. The only data that ever leaves your device is what you explicitly send to the AI, and only when you choose to use that feature.\r\n\r\nThere's a whole population of people training in gyms with patchy WiFi, in basements, in garages, in parks, who have never once had Musclog fail because it needed to call home. That matters to me more than a monthly recurring revenue number.\r\n\r\n## What's next\r\n\r\nOnboarding is the weakest part of the app right now. Musclog is significantly more useful the more data it has, but the new user experience is still too close to \"here's the entire app, figure it out.\" I want to build a proper onboarding flow that gets someone set up with their goals, their first workout template, and their first food log in under five minutes. You shouldn't need 500 hours of context to get value from day one.\r\n\r\nBLE device support for smart scales is also on the list. Manually entering weight and body fat every day is fine. Having the scale send it to the app automatically when you step on it is better, and the infrastructure isn't that complex. It's mostly a matter of getting my hands on hardware to test against, which is a very solvable problem and definitely something I'll do as soon as I finish the seventeen other things I've already started.\r\n\r\n## Will it be free forever?\r\n\r\nFair question. The short answer is: I have no idea. The longer answer is more interesting.\r\n\r\nThe code is open source under Attribution-NonCommercial-NoDerivatives 4.0 International. You can read it, fork it for personal use, learn from it. You just can't ship a product built on it without talking to me first, because Musclog is a proprietary brand that I own, and because the five hundred hours of work behind it represent something north of €25000 in European software engineering salaries. I'm not running a charity. I'm running a side project that hasn't asked you for money yet.\r\n\r\nIf that ever changes, here's my promise: I won't do the subscription thing. If Musclog ever costs money, it'll cost money once. You pay the price of a coffee, the app is yours, forever. No recurring fees. No \"your premium plan has been paused.\" The good old model, the one the App Store basically killed.\r\n\r\nThe thing pushing me closest to that scenario right now is iOS. My girlfriend wants it on iPhone. Some close friends want it on iPhone. I've been hearing \"just put it on the App Store\" for months as if it's a trivial thing. The Apple developer program costs 100 USD a year. Per year. Not once. Every year. Apple built a SaaS out of the right to distribute software, and I say this as someone who thinks Google Play's one-time 35 USD fee is how it should work everywhere. So yes, if Musclog ever lands on iOS, it'll probably have a price tag, because I am not paying a 100 USD annual toll to Apple out of goodwill.\r\n\r\nFor now: Android, free, no plans to change that. So grab it while it's free, because once you download it on Google Play, it's yours forever. Even if you didn't pay for it. Especially if you didn't pay for it.\r\n\r\n## Conclusion\r\n\r\nTwo years, two versions, 500 hours. Musclog finally looks like something I don't mentally apologize for before showing to someone. The redesign cleared years of \"just ship it\" UI debt, and doing it properly forced me to think about the product in ways I should have been thinking about since the beginning. Turns out design systems exist for good reasons and not just to give designers something to talk about in Figma.\r\n\r\nIt's free. It's open source. It doesn't have your data. It works in airplane mode. Real food databases. AI that actually knows who you are. Automated weekly check-ins. And a dark green color scheme I'm genuinely proud of, even if an AI tool did most of the visual heavy lifting.\r\n\r\nIf you want to try it: [Google Play](https://play.google.com/store/apps/details?id=com.werules.logger).\r\n\r\nIf you want to see how it's built or contribute something: [github.com/blopa/musclog-app](https://github.com/blopa/musclog-app).\r\n\r\nLift, Log, Repeat.\r\n\r\nSee you in the next one!\r\n","frontmatter":{"tags":["react","react native","expo","android","fitness","musclog","nutrition","usda","open food facts","stitch","design","offline","watermelondb","nativewind"],"categories":["coding"],"allowComments":true,"publishOnMedium":false,"cover":null,"date":"2026-03-22T00:00:00.000Z","id":null,"path":"musclog-redesign-nutrition-tracking-and-why-your-fitness-app-subscription-is-a-scam","show":true,"title":"Musclog: Redesign nutrition tracking and why your fitness app subscription is a scam","hideExcerpt":false,"subtitle":"New UI, real food databases, and a rant about fitness app subscriptions I've been sitting on for two years"}},"language":"pt-br","intl":{"language":"pt-br","languages":["en","pt-br"],"messages":{"site_title":"pablo.gg","title":"Título","author":"@thepiratepablo","search_placeholder":"Buscar...","about":"Sobre","photos":"Fotos","archive":"Arquivo","contact":"Contato","close":"Fechar","contact_page":"Página de contato","see_more":"Veja mais posts","built_with":"Feito com ","buy_me_a_soda":"Me pague uma bebida","blog":"Blog","blog_posts":"Posts do blog","go_to_post":"Ir para o post","search":"Busca","loading":"Carregando...","search_results":"Resultados de busca","search_results_for":"{quantity} resultados de busca para: \"{query}\"","search_for_query":"Buscar por \"{query}\"","no_results":"Sem resultados","home":"Home","description":"Apenas mais um blog pessoal","go_back":"Voltar para home","thats_me":"Esse sou eu!","got_it":"Entendi!","check_it_out":"Confira!","we_are":"Faltam","e3":"E3","away_from_next_sgf":"para a Summer Game Fest 2026","away_from_next_gamescom":"para a Gamescom 2026","sgf_countdown":"Contador para Summer Game Fest","gamescom_countdown":"Contador para Gamescom","e3_paragraph_1":"Esta página já contou com uma contagem regressiva para o próximo evento E3, um momento que inúmeros jogadores e profissionais da indústria aguardavam ansiosamente a cada ano. E3 não era apenas um evento; era uma celebração da nossa paixão compartilhada por videogames, um lugar onde sonhos se realizavam e memórias eram feitas.","e3_paragraph_2":"Desde os anúncios eletrizantes até as demos práticas, a E3 era o coração do mundo dos jogos. Ela reunia pessoas de todos os cantos do globo, unidas pelo amor aos jogos. Para muitos, era uma chance de conhecer seus heróis, descobrir novos títulos e experimentar a emoção das últimas inovações em tecnologia de jogos.","e3_paragraph_3":"No entanto, à medida que o cenário dos jogos evoluiu, também evoluiu a maneira como nos conectamos e celebramos nossa paixão. Embora a E3 tenha chegado ao fim, o espírito de excitação e comunidade que ela promoveu continua vivo. Agora esperamos por novas maneiras de nos reunir, compartilhar nosso amor pelos jogos e criar novas memórias.","e3_paragraph_4":"Embora a contagem regressiva tenha acabado, o legado da E3 permanecerá para sempre em nossos corações, nos lembrando das jornadas incríveis que fizemos e dos laços que formamos ao longo do caminho.","sec":"Seg","secs":"Segs","min":"Min","mins":"Mins","hour":"Hora","hours":"Horas","day":"Dia","days":"Dias","month":"Mês","months":"Meses","year":"Ano","years":"Anos","recent_posts":"Posts recentes","email":"E-mail","twitter":"Twitter","name":"Nome","page":"Página","fill_this_want_reply":"Preencha isso se quiser que eu entre em contato com você","sorry_this_post_unavailable_language":"Desculpe, este post não está disponível no idioma que você escolheu","language":"Idioma","comment":"Comentário","comments":"Comentários","no_comments":"Nenhum comentário.","post_comment":"Publicar comentário","send_message":"Enviar mensagem","message":"Mensagem","post_a_comment":"Publicar um comentário","your_comment_submitted":"Seu comentário foi enviado com sucesso.","your_message_submitted":"Sua mensagem foi enviada com sucesso.","on":"em","ok":"Ok","copy":"Copiar","copied":"Copiado","photo_num":"Foto {num}","the_matrix_has_you":"The Matrix has you...","about_paragraph_1":"Entusiasta de tecnologia desde pequeno, sempre me interessei por computadores e videogames.","about_paragraph_2":"Me formei em Tecnologia da Informação na Faculdade Estácio de Sá e sempre procuro me informar sobre novas tecnologias e me envolver em novos projetos de desenvolvimento, alguns deles o código fonte pode ser encontrado no GitHub.","cookie_banner_consent":"Ao usar este site, você concorda com o uso de cookies para oferecer uma melhor experiência.","written_in":"Escrito em ","no_post_this_tag":"Nenhum post em Português contém essa tag.","tags":"Tags","tag_colon":"Tag: ","tags_colon":"Tags: ","posts_tagged":"Posts com a tag ","categories":"Categorias","category":"Categoria","category_colon":"Categoria: ","posts_on_category":"Posts na categoria ","related_posts":"Posts relacionados","read_time":"🕒 {time} min. de leitura","create_post":"Criar Post","show":"Mostrar","date":"Data","download":"Download","add_tag":"Adicionar Tag","hide_excerpt":"Esconder Excerpt","publish_on_medium":"Publicar no Medium","allow_comments":"Permitir Comentários","subtitle":"Subtítulo","you_must_be_truly_desperate":"Você deve estar muito desesperado para me pedir ajuda","game.game_title":"pablo.gg - O Jogo","game.next":"Próxima","game.ok":"Ok","game.loading_asset_colon":"Carregando asset:","game.loading":"Carregando...","game.characters.npc_01":"Frost","game.characters.npc_02":"Gavin","game.characters.npc_03":"Giles","game.characters.npc_04":"Godfrey","game.characters.npc_05":"Hugh","game.characters.npc_06":"Ivar","game.characters.npc_07":"Leopold","game.characters.npc_08":"Lucian","game.characters.npc_09":"Gumercindo","game.characters.npc_10":"Mr. Flower","game.characters.npc_11":"Maxim","game.characters.npc_12":"Milo","game.characters.npc_13":"Otto","game.characters.npc_14":"Palmer","game.characters.npc_15":"Quentin","game.characters.npc_16":"Sebastian","game.characters.npc_17":"Neville","game.characters.npc_18":"Cassian","game.characters.npc_19":"Balthasar","game.characters.npc_20":"Jasper","game.characters.sign_01":"Placa","game.characters.book_01":"Livro","game.characters.home_page_city_sign":"Placa","game.characters.coding_category_city_sign_01":"Placa","game.characters.coding_category_city_sign_02":"Placa","game.characters.events_category_city_sign":"Placa","game.characters.funny_category_city_sign":"Placa","game.characters.gadgets_category_city_sign_01":"Placa","game.characters.gadgets_category_city_sign_02":"Placa","game.characters.games_category_city_sign":"Placa","game.characters.general_category_city_sign":"Placa","game.characters.tips_category_city_sign":"Placa","game.characters.collectibles_category_city_sign":"Placa","game.characters.sword":"Info","game.characters.push":"Info","game.gamepad.a_button":"Botão A","game.gamepad.b_button":"Botão B","game.gamepad.d_pad_left":"D-Pad Esquerda","game.gamepad.d_pad_up":"D-Pad Cima","game.gamepad.d_pad_right":"D-Pad Direita","game.gamepad.d_pad_down":"D-Pad Baixo","game.gamepad.start_button":"Botão Start","game.menu.start":"Começar","game.menu.exit":"Sair","game.menu.settings":"Configurações","game.start_menu.save_game":"Salvar Jogo","game.start_menu.exit":"Sair","game.game_over.game_over":"Game Over","game.game_over.retry":"Tentar Novamente","game.game_over.exit":"Sair","game.browse_posts.choose_a_post":"Escolha um post para ler","game.dialogs.npc_01.01":"Ei, você finalmente acordou!","game.dialogs.npc_01.02":"O quê, você não sabe onde está?","game.dialogs.npc_01.03":"Não seja bobo, você está na Cidade Página Inicial, lembra?","game.dialogs.npc_01.04":"Esta cidade foi fundada por Pablo Montenegro para ser o início de sua jornada","game.dialogs.npc_01.05":"Explore o mundo e encontre outras cidades onde você possa ler o conhecimento acumulado de nossa civilização...","game.dialogs.npc_01.06":"... algumas pessoas chamam de \"Posts do Blog\", não sei por quê...","game.dialogs.npc_02.01":"Tenha cuidado com os Slimes que vivem no mundo aberto.","game.dialogs.npc_02.02":"Pressione ESPAÇO para usar sua espada","game.dialogs.npc_02.03":"O que é ESPAÇO? Eu não faço ideia.","game.dialogs.npc_03.01":"Olá, bem vindo a nossa biblioteca","game.dialogs.npc_03.02":"Temos apenas um livro, que contém todas as publicações da categoria dessa cidade.","game.dialogs.npc_03.03":"Vá dar uma olhada!","game.dialogs.npc_04.01":"Eu gosto de caracóis","game.dialogs.npc_05.01":"Frases incompletas podem causar","game.dialogs.npc_06.01":"O vermelho é mais verde do que o roxo, com certeza.","game.dialogs.npc_07.01":"Ter barba é o novo não ter barba","game.dialogs.npc_08.01":"E aí","game.dialogs.npc_09.01":"\" - Cooper, o que está fazendo?\"\n\" - Atracando.\"","game.dialogs.npc_10.01":"Eu deveria comprar um barco","game.dialogs.npc_11.01":"Conhece a piada do não nem eu? Não? Nem eu!","game.dialogs.npc_12.01":"Eu limpo o banheiro e resgato a princesas, vida boa, certo?","game.dialogs.npc_13.01":"Queremos as ondas de rádio de volta","game.dialogs.npc_14.01":"Salve a líder de torcida, salve o mundo","game.dialogs.npc_15.01":"Olá, como vai?","game.dialogs.npc_15.02":"OK, tchau!","game.dialogs.npc_16.01":"Um canguru é realmente apenas um coelho com esteróides","game.dialogs.npc_17.01":"Pela 216ª vez, ele disse que pararia de beber refrigerante após esta última Coca","game.dialogs.npc_18.01":"Nada e tudo é possímpossivel","game.dialogs.npc_19.01":"Para uma cidade chamada \"Eventos\", não há muito acontecendo...","game.dialogs.npc_20.01":"Eu ouvi dizer que existe um jeito de empurrar alguns objetos neste jogo, mas não sei como.","game.dialogs.sign_01.01":"Parabéns, você pode ler isso!","game.dialogs.book_01.01":"Hey, obrigado por testar esse novo jeito bem esquisito de acessar o meu site","game.dialogs.book_01.02":"Este projeto não seria possível sem o incrível trabalho de muitas pessoas, como:","game.dialogs.book_01.03":"ArMM1998 - Pelos sprites dos personagens e os tilesets","game.dialogs.book_01.04":"PixElthen - Pelos sprites do slime","game.dialogs.book_01.05":"pixelartm - Pelos sprites do chapéu de pirata","game.dialogs.book_01.06":"jkjkke - Pela imagem da tela de Game Over","game.dialogs.book_01.07":"KnoblePersona - Pela imagem da tela do menu inicial","game.dialogs.book_01.08":"Min - Pelo sprite do livro aberto","game.dialogs.book_01.09":"E claro, ao Richard Davey por ter criado o Phaser.io!","game.dialogs.home_page_city_sign":"Cidade Página Inicial","game.dialogs.coding_category_city_sign.01":"Cidade da Categoria Programação","game.dialogs.coding_category_city_sign.02":"Cidade da Categoria Programação","game.dialogs.events_category_city_sign":"Cidade da Categoria Eventos","game.dialogs.funny_category_city_sign":"Cidade da Categoria Engraçado","game.dialogs.gadgets_category_city_sign.01":"Cidade da Categoria Eletrónicos","game.dialogs.gadgets_category_city_sign.02":"Cidade da Categoria Eletrónicos","game.dialogs.games_category_city_sign":"Cidade da Categoria Jogos","game.dialogs.general_category_city_sign":"Cidade da Categoria Geral","game.dialogs.tips_category_city_sign":"Cidade da Categoria Dicas","game.dialogs.collectibles_category_city_sign":"Cidade da Categoria Brinquedos","game.dialogs.sword_item_description":"Agora você pode atacar, pressione ESPAÇO para usar sua espada.","game.dialogs.push_item_description":"Agora você pode empurrar alguns objetos, pressione ESPAÇO na frente de um objeto para empurrá-lo.","zelda_timeline.title":"Timeline de Zelda","zelda_timeline.timeline_split":"Divisão da Timeline","zelda_timeline.timeline_unification":"Timeline Unificada","zelda_timeline.icons_from":"Os ícones usados nesta página são do zeldauniverse.net e game-icons.net","zelda_timeline.creation":"Criação","zelda_timeline.creation_of_land_sky":"A Criação da Terra e do Céu","zelda_timeline.goddess_hylia_and_sky_era":"Deusa Hylia e a Era do Céu","zelda_timeline.skyward_sword":"Skyward Sword","zelda_timeline.the_ancient_battle":"A Antiga Batalha e a reencarnação da Deusa Hylia","zelda_timeline.return_to_surface":"O retorno à superfície","zelda_timeline.era_of_chaos":"Era do caos","zelda_timeline.sacred_realm_sealed":"O Sacred Realm é selado","zelda_timeline.era_of_prosperity":"Era da Prosperidade","zelda_timeline.establishment_of_hyrule":"O Reino de Hyrule é estabelecido","zelda_timeline.force_era":"Era da Força","zelda_timeline.the_minish_cap":"The Minish Cap","zelda_timeline.rise_of_evil_vaati":"A Ascensão do Maligno Vaati","zelda_timeline.four_swords":"Four Swords","zelda_timeline.resurrection_of_vaati":"A Ressurreição de Vaati","zelda_timeline.era_of_the_hero_of_time":"Era do Herói do Tempo","zelda_timeline.hyrulean_civil_war":"Guerra Civil Hyruleana","zelda_timeline.ocarina_of_time":"Ocarina of Time","zelda_timeline.sacred_realm_becomes_dark_world":"O Sacred Realm se torna o Dark World","zelda_timeline.ganondorf_becomes_ganon":"Ganondorf se torna Ganon","zelda_timeline.hero_is_defeated":"O herói é derrotado","zelda_timeline.decline_of_last_hero":"O declínio de Hyrule e o último herói","zelda_timeline.the_imprisoning_war":"A Guerra do Aprisionamento","zelda_timeline.era_of_dark_and_light":"Era de Luz e Escuridão","zelda_timeline.a_link_to_the_past":"A Link to the Past","zelda_timeline.resurrection_of_ganon":"A Ressurreição de Ganon","zelda_timeline.resurrection_of_ganon_is_prevented":"A ressurreição de Ganon é evitada","zelda_timeline.links_awakening":"Link's Awakening","zelda_timeline.oracle_of_ages_and_seasons":"Oracle of Ages e Oracle of Seasons","zelda_timeline.a_link_between_worlds":"A Link Between Worlds","zelda_timeline.tri_force_heroes":"Tri Force Heroes","zelda_timeline.the_gold_era":"The Gold Era","zelda_timeline.monarchs_of_hyrule_use_triforce":"Os Monarcas de Hyrule usam a Triforce","zelda_timeline.era_of_decline":"A Era do Declínio","zelda_timeline.tragedy_of_princess_zelda_1":"A Tragédia da Princesa Zelda I","zelda_timeline.the_legend_of_zelda":"The Legend of Zelda","zelda_timeline.adventure_of_link":"Adventure of Link","zelda_timeline.hero_defeated":"Hero Derrotado","zelda_timeline.child_era":"Era Infantil","zelda_timeline.adult_era":"Era Adulta","zelda_timeline.sacred_realm_protected":"Sacred Realm está protegido","zelda_timeline.twilight_realm_and_legacy_of_hero":"O Twilight Realm e o legado do Herói","zelda_timeline.majoras_mask":"Majora's Mask","zelda_timeline.prince_of_thieves_is_executed":"O Príncipe dos Ladrões Ganondorf é executado","zelda_timeline.twilight_era":"A Era do Twilight","zelda_timeline.twilight_princess":"Twilight Princess","zelda_timeline.shadow_invasion":"A invasão das sombras","zelda_timeline.shadow_era":"A Era das Sombras","zelda_timeline.four_swords_adventures":"Four Swords Adventures","zelda_timeline.reincarnation_of_ganondorf":"A Reencarnação de Ganondorf","zelda_timeline.ganondorf_is_sealed":"Ganondorf é selado","zelda_timeline.hero_of_wind_and_new_world":"O Herói do Vento e um Novo Mundo","zelda_timeline.era_without_a_hero":"A era sem um herói","zelda_timeline.ganondorf_is_resurrected":"Ganondorf é ressuscitado","zelda_timeline.hyrule_is_sealed_and_flooded":"Hyrule é selado e então inundado","zelda_timeline.era_of_the_great_sea":"A Era do Great Sea","zelda_timeline.the_wind_waker":"The Wind Waker","zelda_timeline.era_of_the_great_voyage":"A Era da Grande Viagem","zelda_timeline.phantom_hourglass":"Phantom Hourglass","zelda_timeline.era_of_hyrule_rebirth":"A Era do Renascimento de Hyrule","zelda_timeline.new_continent_discovered":"Novo continente descoberto","zelda_timeline.new_hyrule_is_founded":"Um novo reino de Hyrule é fundado","zelda_timeline.spirit_tracks":"Spirit Tracks","zelda_timeline.evil_king_malladus_is_resurrected":"O Rei Maligno Malladus é ressuscitado","zelda_timeline.age_of_calamity":"Age of Calamity","zelda_timeline.breath_of_the_wild":"Breath of the Wild","zelda_timeline.era_of_the_wilds":"A Era dos Selvagens","zelda_timeline.calamity_ganon_is_sealed":"Calamity Ganon é Selado. A tecnologia é proibida, levando alguns Sheikah a formar o Yiga Clan","zelda_timeline.divine_beasts_are_cleansed":"As Bestas Divinas são purificadas e Calamity Ganon é Selado","zelda_timeline.tears_of_the_kingdom":"Tears of the Kingdom","zelda_timeline.hyrule_kingdom_is_teared_apart":"Ganondorf é ressuscitado (Será?)","blog_categories.games":"Jogos","blog_categories.general":"Geral","blog_categories.tips":"Dicas","blog_categories.events":"Eventos","blog_categories.coding":"Programação","blog_categories.funny":"Engraçado","blog_categories.collectibles":"Colecionáveis","blog_categories.gadgets":"Eletrónicos","e3_2012_photos.title":"E3 2012","e3_2012_photos.description":"Em junho de 2012 participei da E3 como imprensa para uma cobertura completa para o Nintendo Blast.","e3_2013_photos.title":"E3 2013","e3_2013_photos.description":"Em junho de 2013 participei da E3 como imprensa para uma cobertura completa para o Game Blast.","e3_2014_photos.title":"E3 2014","e3_2014_photos.description":"Em junho de 2014 participei da E3 como imprensa para uma cobertura completa para o Game Blast.","e3_2015_photos.title":"E3 2015","e3_2015_photos.description":"Em junho de 2015 participei da E3 como imprensa para uma cobertura completa para o Game Blast e Game Over TV.","e3_2017_photos.title":"E3 2017","e3_2017_photos.description":"Em junho de 2017 participei da E3 como imprensa para uma cobertura completa para o PlayReplay e Game Over TV.","e3_2019_photos.title":"E3 2019","e3_2019_photos.description":"Em junho de 2019 participei da E3 como imprensa para uma cobertura completa para o PlayReplay.","gamescom_2019_photos.title":"Gamescom 2019","gamescom_2019_photos.description":"Em agosto de 2019 participei da Gamescom como imprensa para uma cobertura completa para o PlayReplay.","san_francisco_2019_photos.title":"San Francisco 2019","san_francisco_2019_photos.description":"Em setembro de 2019, viajei para San Francisco para o show do Metallica S&M2.","forty_two_page.title":"Quarenta e Dois","forty_two_page.description":"Até mais, e obrigado pelos peixes!","projects_page.title":"Projetos","projects_page.description":"Aqui está uma lista de alguns dos meus projetos pessoais favoritos.","projects_page.gatsbyMaterialUiBlogDescription":"Um simples Gatsby Blog Starter com Material UI.","projects_page.contractBuilderDescription":"O Contract Builder é um projeto de código aberto gratuito que permite a qualquer pessoa manter e construir facilmente qualquer tipo de contrato (documentos legais, processos judiciais, aluguel, acordos, construção e assim por diante) usando o Google Spreadsheets. Este foi desenvolvido como um projeto pessoal para ajudar uma amiga que estava com dificuldades de gastar até uma hora para fazer um contrato personalizado, agora ela consegue fazer em menos de 5 minutos. Hooray!","projects_page.resumeBuilderDescription":"Resume Builder é um projeto de código aberto gratuito que permite a qualquer pessoa manter e construir facilmente qualquer tipo de currículo usando o Google Spreadsheets. Este foi desenvolvido como um projeto pessoal para ajudar um amigo que estava com dificuldades de gastar até uma hora para fazer um currículo personalizado.","projects_page.magentoChatbotDescription":"Com este módulo, você pode integrar totalmente a sua loja Magento com os aplicativos de chat mais populares do mercado. Isso significa que simplesmente instalando este módulo e alguns cliques você pode ter uma nova forma de mostrar e vender seus produtos aos seus clientes. Muito fácil de usar! Experimente agora, é GRÁTIS.","projects_page.jamStackSortenerDescription":"Este é um POC de um encurtador de URL básico desenvolvido com Gatsby.","projects_page.gotinhaDescription":"Sempre foi meu sonho fazer meu próprio jogo, e depois de experimentar o Unity alguns anos atrás, decidi tentar novamente com algo com o qual estou mais familiarizado: Javascript. Como desenvolvedor front-end, Javascript já é a linguagem com a qual escrevo a maior parte do meu código no trabalho e também em meus projetos pessoais, e após uma rápida pesquisa consegui encontrar o incrível PhaserJS Framework para construção de jogos web 2D.","notfound.title":"404: Não encontrado","notfound.header":"404 NÃO ENCONTRADO","notfound.description":"Desculpe, esta página parece não existir. Talvez os arquivos estejam incompletos?","seo_keywords.developer":"desenvolvedor","seo_keywords.development":"desenvolvimento","seo_keywords.javascript":"javascript","seo_keywords.es6":"es6","seo_keywords.e3":"e3","seo_keywords.sgf":"sgf","seo_keywords.gamescom":"gamescom","seo_keywords.countdown":"contador","seo_keywords.archive":"arquivo","seo_keywords.about_me":"sobre mim","seo_keywords.personal_blog":"blog pessoal","seo_keywords.personal_projects":"projetos pessoais","seo_keywords.travels":"viagens","seo_keywords.tips":"dicas","seo_keywords.lifehacks":"truques de produtividade","seo_keywords.reviews":"analises","seo_keywords.games":"games","seo_keywords.timeline":"linha do tempo","seo_keywords.photos":"fotos","cookie_law.we_use_cookies":"Usamos cookies para garantir que você obtenha a melhor experiência em nosso site. Ao usar nosso site, você concorda com nossa ","cookie_law.title":"Política de cookies","cookie_law.what_are_cookies":"O que são cookies?","cookie_law.what_are_cookies_text":"Como é prática comum em quase todos os sites profissionais, este site usa cookies, que são pequenos arquivos baixados para o seu computador, para melhorar a sua experiência. Esta página descreve quais informações eles coletam, como as usamos e por que às vezes precisamos armazenar esses cookies. Também compartilharemos como você pode evitar que esses cookies sejam armazenados, no entanto, isso pode diminuir ou 'quebrar' certos elementos da funcionalidade do site. Para obter mais informações gerais sobre cookies, leia ","cookie_law.what_are_cookies_more_info_url":"https://pt.wikipedia.org/wiki/Cookie_(informática)","cookie_law.how_we_use_cookies":"Como usamos cookies","cookie_law.how_we_use_cookies_text":"Usamos cookies por vários motivos detalhados abaixo. Infelizmente, na maioria dos casos, não há opções padrão da indústria para desabilitar cookies sem desabilitar completamente a funcionalidade e os recursos que eles adicionam a este site. Recomenda-se que você deixe todos os cookies se não tiver certeza se precisa deles ou não, caso sejam usados para fornecer um serviço que você usa.","cookie_law.disabling_cookies":"Desativando cookies","cookie_law.disabling_cookies_text":"Você pode impedir a configuração de cookies ajustando as configurações do seu navegador (consulte a Ajuda do navegador para saber como fazer isso). Esteja ciente de que a desativação de cookies afetará a funcionalidade deste e de muitos outros sites que você visita. A desativação dos cookies normalmente resultará na desativação de certas funcionalidades e recursos deste site. Portanto, é recomendável que você não desative os cookies.","cookie_law.the_cookies_we_set":"Os cookies que definimos","cookie_law.site_preferences_cookie":"Cookies de preferências do site","cookie_law.site_preferences_cookie_text":"Para lhe proporcionar uma excelente experiência neste site, fornecemos a funcionalidade para definir as suas preferências de funcionamento deste site quando o utiliza. Para lembrar suas preferências, precisamos definir cookies para que essas informações possam ser chamadas sempre que você interagir com uma página afetada por suas preferências.","cookie_law.third_party_cookies":"Cookies de terceiros","cookie_law.third_party_cookies_text":"Em alguns casos especiais, também usamos cookies fornecidos por terceiros confiáveis. A seção a seguir detalha quais cookies de terceiros você pode encontrar neste site.","cookie_law.third_party_cookies_item_1":"Este site usa o Google Analytics, que é uma das soluções de análise mais difundidas e confiáveis na web para nos ajudar a entender como você usa o site e como podemos melhorar sua experiência. Esses cookies podem rastrear coisas como quanto tempo você passa no site e as páginas que você visita para que possamos continuar a produzir conteúdo envolvente. Para obter mais informações sobre os cookies do Google Analytics, consulte a página oficial do Google Analytics.","cookie_law.third_party_cookies_item_2":"De vez em quando, testamos novos recursos e fazemos mudanças sutis na maneira como o site é fornecido. Quando ainda estamos testando novos recursos, esses cookies podem ser usados para garantir que você receba uma experiência consistente enquanto estiver no site, garantindo que entendemos quais otimizações nossos usuários mais apreciam.","cookie_law.more_information":"Mais informações","cookie_law.more_information_text":"Esperamos que isso tenha esclarecido as coisas para você e, conforme mencionado anteriormente, se há algo que você não tem certeza se precisa ou não, geralmente é mais seguro deixar os cookies ativados, caso eles interajam com um dos recursos que você usa em nosso site. No entanto, se você ainda estiver procurando por mais informações, entre em contato conosco através de nossa "},"routed":true,"originalPath":"/blog/coding/por-que-a-regra-das-7700-calorias-e-falha-e-como-eu-a-corrigi-no-meu-app/medium","redirect":true,"redirectDefaultLanguageToRoot":false,"defaultLanguage":"en","fallbackLanguage":"","ignoredPaths":[]},"blogLocale":"pt-br"}},
    "staticQueryHashes": ["1156153307","1355482417","1591365477","1628619374","2127381735","2288279559","26159077","3566410298","3649515864","3847325417","3982724423","928834867"]}