| 1 | # Calendar |
| 2 | |
| 3 | A calendar for one team, on that team's own instance. Part of the |
| 4 | oficina suite, alongside [docs](https://amadan.net/oficina/docs) and |
| 5 | [sheets](https://amadan.net/oficina/sheets). |
| 6 | |
| 7 | Live at `https://calendar.bds.oncarlos.com`. |
| 8 | |
| 9 | ## What it does |
| 10 | |
| 11 | Day, week, month and agenda views. Events that repeat. Reminders by |
| 12 | email. Import and export as `.ics`, and subscription links you can paste |
| 13 | into Google or Apple Calendar. |
| 14 | |
| 15 | Every screen works with JavaScript switched off — including the week |
| 16 | grid, whose overlapping events are laid out on the server, and |
| 17 | click-a-time-to-create, which is an ordinary link for every half hour of |
| 18 | the day. |
| 19 | |
| 20 | ## What is sealed, and what is not |
| 21 | |
| 22 | **Titles, places and notes are encrypted at rest.** The database file |
| 23 | this instance keeps — the one the platform replicates to object storage |
| 24 | — never holds a readable one. There is a test that greps the raw bytes. |
| 25 | |
| 26 | **Times are not.** They cannot be: every view is a range query over |
| 27 | them, the reminder scheduler reads them before the app has opened |
| 28 | anything, and the platform's own timer registry holds every reminder |
| 29 | instant in plaintext regardless. So: |
| 30 | |
| 31 | > Publish this database and the worst anyone learns is **the shape of |
| 32 | > the team's time** — when people are busy, for how long, how often — |
| 33 | > and who is on the team. Never what any event is, where it is, or what |
| 34 | > was written about it. |
| 35 | |
| 36 | That is a real leak, and a larger one than it first sounds: a team's |
| 37 | free/busy says a lot. It is stated here rather than left for somebody to |
| 38 | discover. |
| 39 | |
| 40 | Three more things sit outside the seal: |
| 41 | |
| 42 | - **A reminder email carries the title.** That is the product working, |
| 43 | but the mail relay's logs are not ours. |
| 44 | - **A subscription link puts this calendar's plaintext into whatever |
| 45 | account you paste it into** — Google, Apple, a phone. The share page |
| 46 | says so before it offers you the button. |
| 47 | - **A compromised running host reads everything.** The instance holds |
| 48 | the key while it runs. **We never claim we cannot read your data.** |
| 49 | |
| 50 | ## Time zones |
| 51 | |
| 52 | An event's record is the wall-clock time you typed plus the zone you |
| 53 | meant it in. The UTC instant is derived from those, and only ever used |
| 54 | as an index. |
| 55 | |
| 56 | That matters when a government moves a daylight-saving boundary and a |
| 57 | new time zone database ships. A calendar that stored the instant would |
| 58 | now have your 09:00 standup at 08:00, with nothing able to say what was |
| 59 | originally meant. Here the standup is still at 09:00 and only the index |
| 60 | moves. |
| 61 | |
| 62 | Your zone is a setting, never guessed from your browser. A calendar that |
| 63 | silently re-times itself because you opened it on holiday is a calendar |
| 64 | that lies to you. |
| 65 | |
| 66 | ## Repeating events |
| 67 | |
| 68 | The repeat picker offers six choices, and they are labelled against the |
| 69 | event you are editing — "Every Wednesday", not "Weekly". |
| 70 | |
| 71 | A calendar you import may use a rule more complicated than those six. |
| 72 | It is kept exactly as it arrived and exported unchanged; its future |
| 73 | occurrences are not drawn on the grid, and the event's page shows you |
| 74 | the rule. Nothing is thrown away. |
| 75 | |
| 76 | ## Reminders |
| 77 | |
| 78 | The instance sleeps when nobody is using it, which is what makes it |
| 79 | cheap to run and what makes reminders interesting. An ordinary timer |
| 80 | inside the process would not fire, because the process is not there. |
| 81 | |
| 82 | So the app tells the platform when it next has work, and the platform |
| 83 | wakes it then. A six-hourly check is the backstop, and any page view |
| 84 | opportunistically sends anything overdue. |
| 85 | |
| 86 | A reminder can still arrive late — a machine has to wake up first. When |
| 87 | that happens the email says so, rather than pretending it was on time. |
| 88 | |
| 89 | ## Exports and subscriptions |
| 90 | |
| 91 | `.ics` export carries IANA time zone identifiers and **no `VTIMEZONE` |
| 92 | block**. Google Calendar, Apple Calendar and Thunderbird resolve zones |
| 93 | from their own databases and are fine. **Outlook desktop may not be** — |
| 94 | it has historically resolved only the zones it finds inside the file. If |
| 95 | you import one of our files into Outlook and see a wrong time, that is |
| 96 | why, and it is worth telling us. |
| 97 | |
| 98 | A subscription link is a capability: anyone holding it can read that |
| 99 | calendar, because a calendar app cannot sign in. Only its hash is |
| 100 | stored, so it is shown once when you create it and never again. Revoke |
| 101 | it and it stops working. |
| 102 | |
| 103 | Subscribed clients poll constantly — Apple roughly every fifteen minutes |
| 104 | — and each poll wakes this instance. The share page shows you how many |
| 105 | times each link has been polled, because a feed is not free. |
| 106 | |
| 107 | ## Running it |
| 108 | |
| 109 | ```sh |
| 110 | make ci # vet, gofmt, tests, migration check — the one gate |
| 111 | make release # the stripped linux/arm64 binary |
| 112 | ``` |
| 113 | |
| 114 | Locally: |
| 115 | |
| 116 | ```sh |
| 117 | go run ./cmd/calendar -env development -addr localhost:8099 -db /tmp/calendar.db |
| 118 | ``` |
| 119 | |
| 120 | `-env development` is the only way to get development behaviour: it is |
| 121 | never honoured from the environment, because platform config *is* |
| 122 | environment variables and a stray `CALENDAR_ENV=development` in |
| 123 | production would open the allowlist and put sign-in links in the log. |
| 124 | |
| 125 | In production the app refuses to start without `CALENDAR_ORIGIN`, |
| 126 | `CALENDAR_INSTANCE_KEY`, `CALENDAR_DEK`, `CALENDAR_ALLOWED_ADDRESSES` |
| 127 | and a mail relay. Each refusal names what to set. A misconfigured |
| 128 | instance is down rather than quietly wrong. |
| 129 | |
| 130 | ## Deploying |
| 131 | |
| 132 | ```sh |
| 133 | make release |
| 134 | carlos deploy --account oficina --app calendar --channel edge \ |
| 135 | --timeout 5m releases/calendar-linux-arm64 |
| 136 | ``` |
| 137 | |
| 138 | Then check the **process**, not the edge: |
| 139 | |
| 140 | ```sh |
| 141 | curl -sS https://calendar.bds.oncarlos.com/api/version |
| 142 | ``` |
| 143 | |
| 144 | `X-Carlos-Version` is the router's adopted release and flips the moment |
| 145 | the promote lands. The process takes about another 110 seconds to cycle. |
| 146 | |
| 147 | `stable` needs a passkey step-up in the console, and so does moving an |
| 148 | instance between `stable` and `edge`. This app runs on `edge`. |
| 149 | |
| 150 | ## Reading further |
| 151 | |
| 152 | - `AGENTS.md` — the binding rules, and the reasoning behind each |
| 153 | - `PRODUCT.md` — what this is for and what it deliberately is not |
| 154 | - `docs/superpowers/specs/` — the design |
| 155 | - `docs/superpowers/brainstorms/` — the raw material, including the |
| 156 | alternatives that were rejected and why |
| 157 | |