Shutting down Workbeam
Today I shut down Workbeam, a side-project I worked on sporadically over the last two years. The premise was simple, what happens when you make project management software as easy to use as slack and fully programmable. While I had no delusions that this would turn into a Jira killing software company, I am a bit dissappointed about how it ended—with a failure to launch.
I originally had the idea when I was changing jobs and spending increasingly more of my time staring at Jira. Why was it that every team I've worked with eventually ends up on Jira? Why was Jira unanimously hated by the teams that used it? Finally, why did they continue to use it!? This led to an exploration of what could be better.
The most important aspect I identified was the ease of capturing work. No project management tool is useful if it doesn't capture and represent your team's work. Jira is pretty stodgy in this regard—adding tickets is slow with many fields you might need to fill out with varying ease-of-use.
The next important aspec is the ability to mold it into your workflow. The best tools become extensions of the team and enhance their abilities in some dimension. Jira on the other hand is often a game of fitting your workflow into it because of how restrictive it can be—specifically features related to kanban and agile. I'd like to take the parts the team likes and evolve process.
What emerged was two primary principles for this project 1) it should be rediculously easy to capture work and 2) it should be maximally expressive of your workflow.
I went with my usual Clojure(Script) stack along with Postgres as the foundation. It was generally a pleasure to be in my preferred repl-driven approach to build a proof of concept. Since this was a side-project, I went much deeper in learning websockets, SQL (learning for real this time), and core.spec for generative testing. I was particularly proud of a somewhat novel entity-component data model which made it really easy to extend the core message model.
For services, I used AWS for hosting (EC2, ELB, RDS, Lambda) and Stripe for integrating payments (it was a great experience to dog food it!). The AWS cost was a bit high for something that had such low utilization at roughly ~$50/month. I probably should have bought a reserved instance, but I wanted to stay flexible so I never did (it ended up costing more because my hosting needs barely changed at all).
The time I set aside to work on the project was usually during commuting and the occasional weekend. This meant I needed to be super disciplined about scoping out features and changes so I could jump back in and finish a task before time ran out (at the time I was commuting on the train). I looked forward to working on it and I think this was in no small part due to splitting the work up into small 30-40 minute chunks.
Ultimately, I built out a full product you could even pay for! It had many features that you might find in a project management tool, but also a flare of something new—programmable hooks you could code up right in the browser, Alfred-style search, and a real-time platform for managing streams of work. Pretty cool!
I meticulously tracked every minute I worked on this project. I originally did it to get a sense for how long it actually takes to build certain features. Having a good mental model for how long something takes is extremely useful to my day job and engineering in general.
Here's the full org-mode timesheet, all 404 (lol) hours of it:
Headline Time ------------------------------------------------------ *Total time* *404:55* ------------------------------------------------------ Landing page 15:17 Test suite 14:21 Set log level appropriately for the app 1:22 Fix fetch beam does not respect beam... 0:16 Launch mvp 0:03 Partial full text search match 0:55 Column view 0:03 Recipe ideas 0:20 At mentions 0:09 Change public form to be per beam... 0:18 Test a lambda in the browser 0:37 Initial API 1:12 Message grammar 0:33 New account signup should redirect to... 1:06 Fix cljs nrepl not working 0:32 Up and down arrows should do the same... 0:22 Capture templates 0:44 Resolve warnings about unique element... 0:05 Middleware for handling messages 0:06 Syntax highlighting for code blocks 0:32 Profile slow rendering of markdown 0:11 Client side caching 0:01 Blog post 0:25 Setup a second server and document it 0:15 Fix signup fails due to spec failure 0:23 Fix update todo not working 0:07 Fix authentication redirects point to... 0:46 Route username and password... 2:03 UI for configuring beam 3:00 Fix dependency mismatch in cider... 0:58 Sort beams in sidenav by name 0:03 Fix creating a new beam doesn't work 0:01 Set up email for the domain 0:21 Forward all traffic to HTTPS for... 0:58 Public personal landing pages that... 2:14 Create a team as part of signup 0:20 Fix debug param not being passed in 0:01 Take debug logging as a param 0:04 Set up a server 2:19 Check specs when updating components 0:06 Spec for lambda function component 0:02 Break apart `workbeam.server' into... 0:23 Show errors when a message handler fails 1:14 Resolve spec errors 1:05 Validate component state when saving... 2:12 Investigate which storage backend to use 0:10 Modal dialog component 1:13 Call the dispatch handler before... 2:29 Save the source entity of a message 0:03 Change `:team-id' to a component 0:33 Use Clojure v1.9 to use spec 0:09 Add spec for message format to... 0:30 Update color scheme 0:31 Add dummy data when developing that... 4:34 Add read receipt component 1:10 Handle assigning a unique ID to the user 0:35 Clock in and out 1:49 Always update the time stamps of... 1:01 Notify when a message is saved or... 1:17 Fixed height message area 0:30 Fix scrolling after submitting new todo 0:35 Add status to tasks 0:40 Create a beam 1:47 Markdown support 0:27 Auto expand the input box if there is... 0:17 Highlight currently selected beam 0:12 Style sidebar beam list 0:12 Update `fetch-msgs!' to take an... 0:09 Beams can have logic of who to send... 5:20 Refer to beams by their ID not name 0:54 Fix loading message not centered 0:10 Show the user's inbox 3:29 Notifications 11:30 Fix logging/printing doesn't work on... 0:07 Link to join team 3:11 Beam list should only show beams... 1:36 Enable keyboard navigation and shortcuts 2:37 List of users on the team 2:16 Fix notifications not working with... 0:06 Update the `:teams-list' event to... 0:45 Fix race condition in loading data... 0:25 Fix signing up to a team destroys... 0:18 Add source to todo rendering 0:11 Fix multiple users notifications not... 0:41 Fix styles of praline inspector 0:09 Update test data 0:03 Add a top nav bar 0:18 Runtime configuration 0:37 Fix login doesn't set the user ID in... 0:18 Add google analytics 1:07 Add inspect view 4:12 Fix doesn't work on Mobile Safari 0:29 User can't see new beams on page refresh 0:32 Production settings on deploy 0:05 Fix nginx redirect of root domain 0:18 Persist the in-memory db on disk... 1:10 Invoke a js function anytime a new... 6:58 Trigger handler of receiving beam to... 3:51 Handle cycles in sending messages 0:02 Comment component 2:41 Switch to UUIDs instead of gensym 1:23 Comment beams showing up in beam list... 0:28 Style the comments 0:31 Clicking a message should also show... 0:02 Close the inspector link 0:08 Make the app responsive 0:33 Fix C-c c should not input the "c"... 0:07 Fix notify-lookup values should... 0:12 Hide the inspector panel if there is... 0:25 Cancel an item 0:29 C-n C-p should not be active when in... 0:50 Fix malformed URL redirect 1:30 Clicking on a team member should open... 1:54 Keyboard navigation 4:01 Install figwheel sidecar to get a... 1:03 Validate uniqueness of username on... 3:04 Handle signup errors in validating 1:08 Signup or joining a team should log... 0:43 Add praline handler for UUID types 0:18 Show the names of users instead of... 1:17 Update design of the app 1:28 Fix inspect message and beam edit... 0:11 Browser notifications 2:16 Flakey server side push notifications 0:22 Don't show praline in production... 0:29 Increase the amount of time a user... 0:13 Login needs to be per team 1:45 Fix up modal design 1:14 Show login errors on login page 1:04 DB backend [51/51] 51:59 Set up db server on aws 0:35 DB connection pooling 0:43 Fix sticky todo input doesn't work... 0:44 Fix width of todo input text area 1:09 Fix public beam form 1:44 Static files not being served 0:02 Replace friend auth to use buddy and jwt 5:33 Handle csrf token errors in signup... 0:31 Fix dispatch strategy for round-robin 1:10 Why does the server start when... 0:03 Cleanup when killing server process 0:12 Update tests after switch to postgres 4:37 Fix missing my incoming beam in the... 0:15 Compile production js 0:30 Fix the DNS so root domain redirects... 0:08 Show error message after being... 0:10 AWS Lambda function that evals js 11:03 Docs for lambda handlers 1:08 Add helper method for Lambda to... 0:17 Fix client fails when trying to... 0:47 Fix height of site pages 0:23 Database insert overrides doesn't... 0:16 Add beam slug to beam-meta which... 2:23 Get email address during signup 0:30 Search and switch to beams using Cmd-k 4:55 C-n C-p to go through search results,... 1:01 Email for beta users 0:26 Fix comment notification shows... 0:01 Notifications should only be received... 0:02 Lambda should allow return of slugs... 0:17 Update styles for status component 0:21 Creating a new todo should not... 1:58 Lambda handler custom messages 5:55 Fix reloading react base components... 0:46 First time user walkthrough 1:54 Codemirror text input 1:22 Clear user status 0:12 Show what currently working on 7:45 Fix invite link should use slug domain 0:17 Fix jumping scrolling of message... 0:42 Fix jumping message container 0:25 Add icon for periodic beam task 1:12 Handle expired session in web app 2:18 Pricing page 1:49 Collect payment 8:42 Macro for component specs 2:02 Test invite code validation 0:14 Periodic handlers 24:47 API endpoint for creating a todo 5:48 Invite code 2:12 Fix welcome modal not showing 0:07 Fix positional selected message is... 0:49 Fix design of promo on homepage 0:07 Fix search does not filter for the team 0:06 Filter complete todos from beam 8:21 Add send button to todo-input prompt 1:26 Full text search 5:03 Fix selecting a message that has not... 0:30 Fix positional selected message is... 0:48 Click on working on status to see... 0:03 Test lambda in the browser 4:46 Fix periodic beam code state doesn't... 0:07 Message inspector not working 0:45 Use codemirror for the text input field 1:59 Transaction for signup 1:07 Fix production Stripe keys 0:20 Fix scroll to bottom stopped working 0:15 Fix filtering messsages not working... 0:22 Beam empty dialog not showing up when... 0:47 Breakpoint for small screens 0:51 Move a message 8:35 Fix keyboard select messages 0:28 Don't show message send on comment send 0:08 Make a managed cache for messages 1:53 Assign a todo 9:58 Handle errors in assinging a todo 0:24 Archive a beam 2:12 Add codemirror instance to inspector 0:51 Show something when a beam is empty 0:49 Spec for input types and output types 7:25 Migrate to with-db-stubs 1:01 Check auth token on ws requests 3:12 Re-style modals to not be full screen 0:27 Fix clear current status fails due to... 0:30 Fix filtering out done tasks doesn't... 0:29 Set up test.check generators for... 2:55 Send welcome email 5:32 Fix env variables and `systemctl' 0:20
A few takeaways:
- The hardest feature, by time spent was implementing the DB backend. I spent more time here because I was also learning SQL and some of the specific niceities of Postgres.
- Periodic handlers were a especially tricky, I needed to set up some plumbing for a separate thread (a Clojure
agent) that sweeps over a list of handlers. The trickier part was also handling auth correctly by generating temporary credentials.
- Most features took between 0.5-1.5 hours, which makes sense given most of the work was done on a 40 minute train ride. Still, it's a good reminder that a lot can be done in a short period of time in an ideal environment.
During all this time, I mostly focused on building and exploring rather than working with users and iterating. That led to a common failure mode in which I was perfectly content if it never saw the light of day. I overbuilt, got distracted, and finally lost interest because no one was using it!
There were also fundamental issues I failed to address (or maybe avoided). For example, making it programmable was really cool, but I struggled to find a killer use-case for which it could be applied. Similarly, I didn't quite get the basics down—things like moving tickets around, re-ordering, and admin tools. In hindsight, I should have greatly constrained the MVP to something simpler to test out the key features (capture and programmbility), something like a todo list you could program, to see if it was valuable at all.
My main takeaway is I could have gotten here faster and saved some CPU cycles. I fell in love with building rather than solving valuable user problems. Side projects can be nasty sometimes in how easy it is to say "well, it's just a side-project". I think that's fine as long as you don't fool yourself into thinking it's more. I'm thankful I got to learn many things about the problem space and some new tools. I'm grateful for an outlet for some of my creative energy that Workbeam became and the product lessons I learned (and relearned). Maybe next time, I'll follow my own advice a bit closer.