Coffee Time

Top-level Files of tip

Top-level Files of tip

Files in the top-level directory from the latest check-in

Coffee Time!

A service for scheduling, hosting, and participating in a Lean Coffee conversation.


Java 17 or higher.


NVM (see .nvmrc for version)

How to run


Fast development


This causes the following files to load directly from disk instead of using the classloader (see ResourceType for more):

resources/templates (ResourceType#configureVelocity)
target/classes/* (ResourceType#chooseResourceFetcher)

Run CoffeeTime using the method above. This causes NPM to install and so on.

Then, run: bin/run-dev-watch

This causes a bunch of folders to be watched for changes in parallel and recompiled / deployed on-the-spot.

All at once (Easy Button™)

(requires screen: brew install screen)

If you want a super simple way to do everything at once, properly, simply run bin/dev

To shut it all down (the easy way): bin/stop-dev

To kill everything in the screen (the hard way):

  1. CTRL+c (kill the first side process)
  2. CTRL+a, <tab> (switch to the second side)
  3. CTRL+c (kill the second side process)

Using Podman

You can get a clean podman image tagged as coffeetime by using bin/package


Recommended tooling for local 12-factor : "In the modern era, software is commonly delivered as a service: called web apps, or software-as-a-service. The twelve-factor app is a methodology for building software-as-a-service apps..." : "direnv is an extension for your shell. It augments existing shells with a new feature that can load and unload environment variables depending on the current directory."


Regenerate this section by running bin/docs

The following environment variables can be used to configure the server:

COFFEE_AUTH0_CALLBACK_URI : URI to return to after Auth0 authentication : Defaults to

COFFEE_AUTH0_CLIENT_ID : Which app in Auth0 to authenticate against

COFFEE_AUTH0_CLIENT_SECRET : Proof we are allowed to retrieve user info from Auth0

COFFEE_AUTH0_DOMAIN : Which Auth0 tenant our app is in

COFFEE_AUTH0_MGMT_CLIENT_ID : Used to perform Auth0 management functions like creating a new user

COFFEE_AUTH0_MGMT_CLIENT_SECRET : Proof we are allowed to perform Auth0 management functions

COFFEE_AUTO_WRAPUP_SCHEDULE : Schedule to use to automatically wrap up finished events : Defaults to 0 0 0 * * ?

COFFEE_BASE_URL : The base url that we append to when generating links for activation, opt out, etc. : Defaults to http://localhost:3000

COFFEE_DB_LOC : Location of our database : Defaults to coffeetime.db

COFFEE_EVENT_HOST_EMAIL_SCHEDULE : Schedule to use to send host emails : Defaults to 0 0 * ? * *

COFFEE_EVENT_NOTIFICATION_ENABLED : Does the server support event notifications? : Defaults to true

COFFEE_EVENT_PARTICIPANT_EMAIL_SCHEDULE : Schedule to use to send participant emails : Defaults to 0 0 10 ? * *

COFFEE_EVENT_TEAM_CHAT_EMAIL_SCHEDULE : Schedule to use to send team chat emails : Defaults to 0 0 */8 ? * *

COFFEE_GOOGLE_CLIENT_ID : Used to enable Google authentication

COFFEE_GOOGLE_CLIENT_SECRET : Proof we are allowed to authenticate Google users

COFFEE_JOB_THREADS : How many threads can be allocated to running background jobs : Defaults to 1

COFFEE_LINKEDIN_CLIENT_ID : Used to enable LinkedIn authentication

COFFEE_LINKEDIN_CLIENT_SECRET : Proof we are allowed to authenticate LinkedIn users

COFFEE_LIVEKIT_DOMAIN : Domain name to find LiveKit Server at

COFFEE_LIVEKIT_KEY : Name of the key to use to authenticate against the LiveKit server

COFFEE_LIVEKIT_SECRET : Value of the key to authenticate against the LiveKit server

COFFEE_LOG_FILE : If logging to a file, which file to log to : Defaults to ./coffeetime.log

COFFEE_LOG_LEVEL : What level of log messages to emit : Defaults to INFO

COFFEE_LOG_TYPE : What type of logging to perform. 'file' means log to the given log location, otherwise console : Defaults to console

COFFEE_NOTIFICATION : Notification style. You can use print or email. If you use anythingelse, it will default to print. : Defaults to print

COFFEE_PORT : Port to listen on : Defaults to 3000

COFFEE_RESOURCE_TYPE : Where to fetch static resources from. Value values: disk, classloader

COFFEE_SECURE_COOKIES : Should we set cookies securely? Set to 'false' if you are testing locally without SSL : Defaults to true

COFFEE_SENTRY_DSN : Authentication token for

COFFEE_SENTRY_ENV : Value of environment in Sentry : Defaults to prod

COFFEE_SMTP_FROM : Email address for the 'from' address of emails we send


COFFEE_SMTP_PORT : What port the SMTP server listens on : Defaults to 465

COFFEE_SMTP_SERVER : Where to send emails to. Used for email notifications

COFFEE_SMTP_USER : User to authenticate against the SMTP server with

COFFEE_SUPPORT_EMAIL : Email address users should email when they need help : Defaults to

COFFEE_WS_URL : The url we use to connect via WebSocket : Defaults to ws://localhost:3000/

Feature Flags

Sometimes features need to be put onto mainline but disabled so we can continue development work and ensure quality simultaneously. You can add a feature flag by setting an environment variable: COFFEE_FF_<NAME>

Then, refer to <NAME> in template files: ${<NAME>, <VALUE>)}

or in regular code: Env.flagIs(<NAME>, <VALUE>)

One flag that is permanently used in the codebase is COFFEE_FF_MODE. If you set this to dev you'll get fancy links at the bottom of the page to do naughty things like approve all events and move a bunch of them around, etc.

Protocol Buffers

Coffee Time communicates large data using Protocol Buffers. Especially Event information, which can get quite large. This saves a lot of CPU time on encoding and decoding on both the client and server.

Our implementation of Protocol Buffers is ... special. It relies on automatically generated protocol files from annotated Java classes. There are a few gotchas to pay attention to.

First: all fields are marked as optional. This means that any value can be serialized as null across the wire.

Second: the implementation of protocol buffers we are using for JavaScript has a strange quirk. If a field isn't present, it is set to its default value. For strings, that value is '', and for numbers, it's 0. So, in order to tell the difference between 0 and not-present (which usually translates to null), you have to:

v.hasOwnProperty("key") ? v.key : null

instead of just:


Technical Goals

We are attempting to evolve the system toward greater simplicity. To that end:

  1. We should move towards server-side rendering. This will eventually remove our dependence on node for compiling javascript, lit components, etc.
  2. Nothing else yet. I'm sure we'll come up with something!


(c) 2024, Stephen Starkey. All Rights Reserved.