☀️ 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
- Track meals.
And it works on any device that has a browser.
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". Currently, we support login with Apple, Facebook, GitHub, and 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. ReplaceNEXT_PUBLIC_HASURA_DOMAIN
with your own domain. ReplaceHASURA_GRAPHQL_JWT_SECRET
,JWT_SIGNING_SECRET
,HASURA_GRAPHQL_ADMIN_SECRET
, andPOSTGRES_PASSWORD
with your own values. All four of these values are secret and should be kept safe.HASURA_GRAPHQL_JWT_SECRET
andJWT_SIGNING_SECRET
are used to create and verify JWTs. Thesecond-long-secret-value
must be replaced with the same value. And it should be (at least) thirty characters long. As forHASURA_GRAPHQL_ADMIN_SECRET
andPOSTGRES_PASSWORD
, they are both passwords. The former is to sign in to the Hasura console. The latter is to sign to PostgreSQL, the database used by Clean Slate. Also, if you are using a port that differsnginx
orcaddy
(Step #3), you must also change the following items.HASURA_PORT
,AUTHENTICATION_SERVER_PORT
, andCLIENT_PORT
. You must change it from8080
,3001
, and3000
to what you want to use. Otherwise, Clean Slate will not work, and you will get an error when you try to sign in. If desired, you can also change thePOSTGRES_PORT
from5432
as well.
AUTHENTICATION_SERVER_PORT=3001
CLIENT_PORT=3000
HASURA_GRAPHQL_ADMIN_SECRET=first-long-secret-value
HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256","key":"second-long-secret-value"}'
HASURA_PORT=8080
JWT_SIGNING_SECRET=second-long-secret-value
NEXT_PUBLIC_HASURA_DOMAIN=your-server-domain
POSTGRES_PASSWORD=third-long-secret-value
POSTGRES_PORT=5432
- Have your reverse proxy point to
http://localhost:3000
,http://localhost:3001
, andhttp://localhost:8080
. For example, you could useCaddy
and theCaddyfile
below, replacingXXX
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. And keep in mind that your server only needs to expose port443
through the firewall for the app to work. The services run by Docker Compose should not be contacted except via your reverse proxy.
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 "0"
# 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 "0";
# 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 http://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 pull down from the images and start four servers onlocalhost
via Docker Compose. It will also start Caddy. If you do not like any of these behaviors, not a problem! Just modify deploy.sh locally. It is less than ten lines of bash. You can also modify the docker-compose.yml used to deploy Clean Slate.
-
The first server is the database, PostgreSQL, on Docker Hub. Clean Slate uses the default
postgres
user andpostgres
database. It runs this database, Postgres 15, on port5432
via Docker Compose. -
The second server is the GraphQL server, Hasura, on Docker Hub.
-
The third server is the client (busybox). It is built by us and stored on our GitHub Packages.
-
The fourth server is the authentication server (Express.js). It is built by us and stored on our GitHub Packages.
-
Go to the
https://example.com/console
. Make sure to changeexample.com
to value of your actual domain. 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://example.com
with that token. Make sure to changeexample
to value of your actual domain. -
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.
Note: There is a ten-minute lag between each new release and the images being available. That is for two reasons. One, it takes about that long for the GitHub Action building the image to finish, on average. Two, the trigger for that action is the tag itself.
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. -
profiles
: Contains your profile information. 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, apiToken
, 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 apiToken
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 apiToken
, you can just use the Hasura Console. If you would rather not use the apiToken
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/[email protected]" }'
- Production: You need to build the
client
andauthentication-server
images. You cannot use the ones that have already been built. That is because theclient
image has build arguments that are unique to each instance. If you use thedeploy.sh
anddocker-compose.yml
as written, you are set. The images will be built for you automatically. However, if you modify either of those files, you may need to built them yourself.
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