Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone e8a688fd21351d04b05468df
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
policy.json
{
"auth": {
"anonymous": true
},
"people/$userId": {
"description": "A person's Habits settings (the name friends see on shared habits, the first day of the week) as a JSON string. Only that person can read or write it.",
"fields": {
"data": "String"
},
"rules": {
"read": "@user.id != null && @user.id == $userId",
"create": "@user.id != null && @user.id == $userId && @StringUtils.length(@newData.data) <= 2000",
"update": "@user.id != null && @user.id == $userId && @StringUtils.length(@newData.data) <= 2000",
"delete": "false"
}
},
"people/$userId/habits/$habitId": {
"description": "One habit with its schedule and every check-in and note, as a JSON string. Only that person can read or write it. The device keeps the same data, so Habits works offline. A deleted habit stays as a small marker so another device does not bring it back.",
"fields": {
"data": "String"
},
"rules": {
"read": "@user.id != null && @user.id == $userId",
"create": "@user.id != null && @user.id == $userId && @StringUtils.length($habitId) >= 6 && @StringUtils.length($habitId) <= 32 && @StringUtils.length(@newData.data) <= 300000",
"update": "@user.id != null && @user.id == $userId && @StringUtils.length(@newData.data) <= 300000",
"delete": "false"
}
},
"teams/$teamId": {
"description": "A habit shared by link between friends: its name and schedule. Readable by anyone who has opened the link (a join below); the random 20-character id is the link's secret, so nobody can list shared habits.",
"fields": {
"name": "String!",
"kind": "String!",
"target": "UInt!",
"unit": "String!",
"schedule": "String!",
"createdBy": "String!",
"createdAt": "UInt!"
},
"rules": {
"read": "@user.id != null && get(/teams/$teamId/joins/@user.id) != null",
"create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @StringUtils.length($teamId) == 20 && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 60 && (@newData.kind == 'check' || @newData.kind == 'count' || @newData.kind == 'avoid') && @newData.target >= 1 && @newData.target <= 10000 && @StringUtils.length(@newData.unit) <= 20 && @StringUtils.length(@newData.schedule) <= 200 && getAfter(/teams/$teamId/joins/@user.id) != null",
"update": "false",
"delete": "false"
}
},
"teams/$teamId/joins/$userId": {
"description": "Someone who opened a shared habit's link. Knowing the id is what lets you join, and joining only lets you see the streaks on that one habit.",
"fields": {
"at": "UInt!"
},
"rules": {
"read": "@user.id != null && @user.id == $userId",
"create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && getAfter(/teams/$teamId) != null",
"update": "false",
"delete": "@user.id != null && @user.id == $userId"
}
},
"teams/$teamId/members/$userId": {
"description": "One person's streak on a shared habit: the name they chose, current and best streak, its unit (day, week, time, or without for an avoid habit) and the last day they did it (YYYY-MM-DD). Only that person writes it; everyone who opened the link reads it. Nothing else about anyone's habits is shared.",
"fields": {
"name": "String",
"streak": "UInt",
"best": "UInt",
"unit": "String",
"last": "String"
},
"rules": {
"read": "@user.id != null && get(/teams/$teamId/joins/@user.id) != null",
"create": "@user.id != null && @user.id == $userId && getAfter(/teams/$teamId/joins/@user.id) != null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 40 && @newData.best >= @newData.streak && @newData.best <= 100000 && (@newData.unit == 'day' || @newData.unit == 'week' || @newData.unit == 'time' || @newData.unit == 'without') && (@StringUtils.length(@newData.last) == 0 || @StringUtils.length(@newData.last) == 10)",
"update": "@user.id != null && @user.id == $userId && get(/teams/$teamId/joins/@user.id) != null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 40 && @newData.best >= @newData.streak && @newData.best <= 100000 && (@newData.unit == 'day' || @newData.unit == 'week' || @newData.unit == 'time' || @newData.unit == 'without') && (@StringUtils.length(@newData.last) == 0 || @StringUtils.length(@newData.last) == 10)",
"delete": "@user.id != null && @user.id == $userId"
}
},
"constants": {
"AGENT_ADDRESS": "Hw4Dtxudp6vRBEvCyTaHw8Qpv7QfWSHafDNWef5zt24A",
"AGENT_ADDRESS_BOUND": true
}
}
Change history
Point OpenApps links to openapps.free
Bind agent address in policy
Use OpenApp service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync