☀️ Track food without judgment.
Clean Slate is a free calorie tracker. It is designed for people who struggle with:
- Binging
- Self-compassion
- Logging food consistently
- Dieting itself
It can do stuff like:
- Search and log food
- Quick add calories and protein
- Create custom foods and recipes
- Scan barcodes
- Track exercise
To learn more, visit our website or watch the demo video.
On our GitHub Releases page!
Here, we list all the changes that Clean Slate has gone through in each version. Each version covers the enhancements and the security and bug fixes. Each version also outlines any breaking changes, and the steps to migrate, if any. All of this information is especially important for people who want to host Clean Slate on their own.
You do not!
We maintain a free instance at cleanslate.sh. It offers free accounts with social login via Firebase. For example, "Login with Google".
Clean Slate is licensed under Apache 2.0 and is open source!
Hosting Clean Slate is straightforward. You just need a Linux server with Git, Docker, and Docker Compose installed. Make sure to install Docker from the official website 1. That is because the Docker bundled with your distribution is likely out of date.
-
Run
git clone https://github.com/successible/cleanslate
on your server.cd
inside the newly created folder calledcleanslate
. -
Create a
.env
file in thecleanslate
folder. Replace<>
with your values. Thesecond-long-secret-value
ofHASURA_GRAPHQL_JWT_SECRET
andJWT_SIGNING_SECRET
should be the same.
HASURA_GRAPHQL_ADMIN_SECRET=<first-long-secret-value>
HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256","key":"<second-long-secret-value>"}'
JWT_SIGNING_SECRET=<second-long-secret-value>
NEXT_PUBLIC_HASURA_DOMAIN=<your-server-domain>
POSTGRES_PASSWORD=<third-long-secret-value>
- Have your reverse proxy point to
http://localhost:3000
andhttp://localhost:8080
. For example, you could useCaddy
and theCaddyfile
below, replacing<XXX>
with your own domain. The same goes fromnginx
and the samplenginx.conf
below. You could also useapache
or another tool that can act as a reverse proxy. However, Clean Slate must be served overhttps
. Otherwise, it will not work. We just recommend Caddy 2 because it handleshttps
automatically and is easy to use 3.
Here is an example Caddyfile
. Replace <XXX>
with your own domain.
<XXX> {
header /* {
Referrer-Policy "strict-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains;"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block;"
# You can remove the Google, Firebase, and Sentry policies if you are not using them
Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://apis.google.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src 'self' https://*.ingest.sentry.io https://identitytoolkit.googleapis.com https://securetoken.googleapis.com https://apis.google.com https://world.openfoodfacts.org; frame-src 'self' https://*.firebaseapp.com https://www.google.com; img-src 'self' https://www.gstatic.com data:; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; worker-src 'self'; object-src 'none';"
Permissions-Policy "accelerometer=(self), autoplay=(self), camera=(self), cross-origin-isolated=(self), display-capture=(self), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), keyboard-map=(self), magnetometer=(self), microphone=(self), midi=(self), payment=(self), picture-in-picture=(self), publickey-credentials-get=(self), screen-wake-lock=(self), sync-xhr=(self), usb=(self), xr-spatial-tracking=(self)"
}
header /console* {
-Content-Security-Policy
}
route /v1* {
# API (Hasura)
reverse_proxy localhost:8080
}
route /v2* {
# API (Hasura)
reverse_proxy localhost:8080
}
route /console* {
# Admin panel (Hasura)
reverse_proxy localhost:8080
}
route /healthz {
# Health check (Hasura)
reverse_proxy localhost:8080
}
route /auth* {
# Authentication server (Express.js)
reverse_proxy localhost:3001
}
route /* {
# Static files (Clean Slate)
reverse_proxy localhost:3000
}
}
Here is an example nginx.conf
. Replace <XXX>
with your own content.
Note: With
nginx
, you will need to get your own SSL certificate.
error_log /dev/stdout crit;
http {
server {
listen 443 http2 ssl;
listen [::]:443 http2 ssl;
server_name <XXX>;
ssl_certificate <XXX>
ssl_certificate_key <XXX>;
# HTTP Security Headers
add_header Referrer-Policy "strict-origin";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block;";
# You can remove the Google, Firebase, and Sentry policies if you are not using them
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://apis.google.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src 'self' https://*.ingest.sentry.io https://identitytoolkit.googleapis.com https://securetoken.googleapis.com https://apis.google.com https://world.openfoodfacts.org; frame-src 'self' https://*.firebaseapp.com https://www.google.com; img-src 'self' https://www.gstatic.com data:; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; worker-src 'self'; object-src 'none';"
add_header Permissions-Policy "accelerometer=(self), autoplay=(self), camera=(self), cross-origin-isolated=(self), display-capture=(self), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), keyboard-map=(self), magnetometer=(self), microphone=(self), midi=(self), payment=(self), picture-in-picture=(self), publickey-credentials-get=(self), screen-wake-lock=(self), sync-xhr=(self), usb=(self), xr-spatial-tracking=(self)"
location /v1 {
# API (Hasura)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'Upgrade';
proxy_set_header Host $host;
proxy_pass http://localhost:8080;
}
location /v2 {
# API (Hasura)
proxy_pass http://localhost:8080;
}
location /console {
# Admin panel (Hasura)
proxy_pass http://localhost:8080;
add_header Content-Security-Policy "";
}
location /auth {
# Authentication server (Express.js)
proxy_pass localhost:3001;
}
location /healthz {
# Health check (Hasura)
proxy_pass http://localhost:8080;
}
location / {
# Static files (Clean Slate)
proxy_pass http://localhost:3000;
}
}
}
- Run
git pull origin main; bash deploy.sh
. This script will build and start three servers onlocalhost
via Docker Compose. One, the database (PostgreSQL). Two, the client (React via busybox). Three, the GraphQL server (Hasura). Four, the authentication server (Express.js).
Note: Clean Slate uses the default
postgres
user andpostgres
database. It runs this database, Postgres 15, on port5432
via Docker Compose. If you do not like this behavior, you must create your owndocker-compose.yml
. Then, runexport COMPOSE_FILE=<your-custom-file.yml>; git pull origin main; bash deploy.sh
-
Go to the
https://<your-domain>/console
. Log in with yourHASURA_GRAPHQL_ADMIN_SECRET
defined in your.env
. ClickData
, thenpublic
, thenprofiles
, thenInsert Row
. On this screen, clickSave
. This will create a new Profile. Click toBrowse Rows
. Take note of theapiToken
of the row you just made. That is your (very long) password to log in. If you want to create another user, follow the same procedure. Do not share this token with anyone else. It will enable them to access you account. -
You can now log in to
https://<your-domain>
with that token. -
To deploy the newest version of Clean Slate, run
git pull origin main; bash deploy.sh
again. Remember to check GitHub Releases before you deploy!
You can review a GraphQL representation of the documentation here. The documentation is a "live" GraphQL schema in Apollo Studio. You will need to make a free Apollo Studio account to view them.
As you explore the schema, you will see that you can query seven tables using GraphQL.
-
logs
: Contains your logs for food and recipes. See the queries and mutations the app uses. -
quick_logs
: Contains your logs made by "quick adding". See the queries and mutations the app uses. -
exercise_logs
: Contains your logs for exercise. See the queries and mutations the app uses. -
foods
: Contains your basic foods and your custom foods. See the queries and mutations the app uses. -
recipes
: Contains your recipes. See the queries and mutations the app uses. -
ingredients
: Contains your ingredients for recipes. See the queries and mutations the app uses.
Here is an example of the body
for a query
that returns the id
of every log with the unit COUNT
.
{
"token": "XXX",
"query": "query MyQuery($unit: String) { logs(where: {unit: {_eq: $unit}}) { id } }",
"variables": { "unit": "COUNT" }
}
Here is an example of the body
for a mutation
that will add a log of a basic food. You can get the id
of the basic food from the list here.
{
"token": "XXX",
"query": "mutation CREATE_LOG($i: logs_insert_input!) { insert_logs_one(object: $i) { id } }",
"variables": {
"i": {
"alias": null,
"amount": 1,
"barcode": null,
"basicFood": "24bdfa6f-3ab3-46d4-9a57-f78a85128fa3",
"consumed": true,
"food": null,
"meal": "Snack",
"recipe": null,
"unit": "GRAM"
}
}
}
If you want to add a log of a custom food or recipe instead, fine! You will need to set their id
in the food
or recipe
part of the payload. If you want to set a barcode
, you will need to pass these values from the Open Food Facts API.
type Barcode = {
name: string;
code: string;
calories_per_gram: number;
protein_per_gram: number;
calories_per_serving: number;
protein_per_serving: number;
serving_size: number; // "2 Tbsp (30 g)"
serving_quantity: string; // 30
};
Clean Slate was built around delegating authentication to Firebase. Firebase is a very secure authentication service maintained by Google. It is our default recommendation for any instance of Clean Slate with more than a few users. Consult the Using Firebase
section (below) for how to set up Firebase with Clean Slate.
However, Firebase is too complex for the most common hosting scenario. That is a privacy-focused user who wants to host Clean Slate for their personal use. Hence, our default authentication system, authId
, is much simpler. There is no username or password and no need for your server to send email. Instead, we use very long tokens (uuid4) stored as plain text in the authId
column in the database. Because each token is very long and generated randomly, they are very secure. And if you ever need to change the value of the authId
, you can just use the Hasura Console. If you would rather not use the authId
system, you will need to use Firebase instead.
Firebase needs to be configured in three places:
- Your local machine (Local)
- Your production server (Production)
- The Firebase console (Web)
Here is how you do it:
- Web: Create a new Firebase project.
- Web: Enable Firebase authentication.
- Web: Enable the Google provider in Firebase.
- Local: Create the
.firebaserc
in the root with the following content. Example:
{
"projects": {
"default": "<your-firebase-project-name>"
}
}
- Local: Create a
firebase-config.json
locally filled with the content offirebaseConfig
. You can find that on your Project Settings page on Firebase.
{
"apiKey": "<XXX>",
"appId": "<XXX>",
"authDomain": "<XXX>",
"messagingSenderId": "<XXX>",
"projectId": "<XXX>",
"storageBucket": "<XXX>"
}
-
Local: Login with Firebase via
npx firebase login
. -
Local: Run
npx firebase deploy --only functions
. This will deploy Firebase functions in/functions
. -
Production: Add these items to your
.env
on your production server. Replace<XXX>
with your own values. You can find your project config in your Firebase project settings. Do not add these values unless you are doing authentication via Firebase.
NEXT_PUBLIC_FIREBASE_CONFIG='{"apiKey":"<XXX>","appId":"<XXX>","authDomain":"<XXX>","messagingSenderId":"<XXX>","projectId":"<XXX>","storageBucket":"<XXX>"}'
NEXT_PUBLIC_LOGIN_WITH_APPLE='true'
NEXT_PUBLIC_LOGIN_WITH_FACEBOOK='true'
NEXT_PUBLIC_LOGIN_WITH_GITHUB='true'
NEXT_PUBLIC_LOGIN_WITH_GOOGLE='true'
NEXT_PUBLIC_USE_FIREBASE='true'
HASURA_GRAPHQL_JWT_SECRET='{ "type": "RS256", "audience": "<XXX>", "issuer": "https://securetoken.google.com/<XXX>", "jwk_url": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com" }'
- Production: Remove these items from your
.env
:JWT_SIGNING_SECRET
.
Run Clean Slate locally, make changes, and then submit a pull request on GitHub!
Note: Clean Slate is written in React and TypeScript, with Next.js as the framework. It uses Hasura as the backend and PostgreSQL as the database.
Here is how to run Clean Slate locally:
-
Install the following and make sure Docker Desktop is running:
-
Run
pnpm dev
after cloning down the repository. This will spin up these servers:- Hasura (API):
http://localhost:8080
. - Hasura (Console):
http://localhost:9695
. - Next.js:
http://localhost:3000
. - PostgreSQL:
http://localhost:1270
- Hasura (API):
-
Navigate to
https://localhost
and login with token22140ebd-0d06-46cd-8d44-aff5cb7e7101
.
Note: To run Clean Slate with Firebase, do all the
Local
andWeb
outlined above. Install jq locally. Finally, tweak the development command. Runexport FIREBASE='true'; pnpm dev
instead.
Note: To test the deployment process, run
git pull origin main; bash deploy.sh
. Make sure to create the.env
(below) andCaddyfile
(above) first.
Note: To test Clean Slate on a mobile device, install
ngrok
. Runngrok http --host-header localhost https://localhost:443
in another terminal.
# .env for testing the hosting process locally. Do not use in an actual production setting!
HASURA_GRAPHQL_ADMIN_SECRET=XXX
HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256","key":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}'
JWT_SIGNING_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
NEXT_PUBLIC_HASURA_DOMAIN=localhost
POSTGRES_PASSWORD=XXX