Add a CLAUDE.md and a STYLE.md

The CLAUDE.md file is a stripped-down version of a file originally
generated by Claude.

The STYLE.md file is inspired by an internal 37signals style guide.
This commit is contained in:
Mike Dalessio
2025-11-20 10:19:04 -05:00
parent f6cfb5fde4
commit 7ef7a8e49b
3 changed files with 360 additions and 1 deletions
+142
View File
@@ -0,0 +1,142 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is Fizzy?
Fizzy is a collaborative project management and issue tracking application built by 37signals/Basecamp. It's a kanban-style tool for teams to create and manage cards (tasks/issues) across boards, organize work into columns representing workflow stages, and collaborate via comments, mentions, and assignments.
## Development Commands
### Setup and Server
```bash
bin/setup # Initial setup (installs gems, creates DB, loads schema)
bin/dev # Start development server (runs on port 3006)
```
Development URL: http://fizzy.localhost:3006
Login with: david@37signals.com (development fixtures), password will appear in the browser console
### Testing
```bash
bin/rails test # Run unit tests (fast)
bin/rails test test/path/file_test.rb # Run single test file
bin/rails test:system # Run system tests (Capybara + Selenium)
bin/ci # Run full CI suite (style, security, tests)
# For parallel test execution issues, use:
PARALLEL_WORKERS=1 bin/rails test
```
CI pipeline (`bin/ci`) runs:
1. Rubocop (style)
2. Bundler audit (gem security)
3. Importmap audit
4. Brakeman (security scan)
5. Application tests
6. System tests
### Database
```bash
bin/rails db:fixtures:load # Load fixture data
bin/rails db:migrate # Run migrations
bin/rails db:reset # Drop, create, and load schema
```
### Other Utilities
```bash
bin/rails dev:email # Toggle letter_opener for email preview
bin/jobs # Manage Solid Queue jobs
bin/kamal deploy # Deploy (requires 1Password CLI for secrets)
```
## Architecture Overview
### Multi-Tenancy (URL-Based)
Fizzy uses **URL path-based multi-tenancy**:
- Each Account (tenant) has a unique `external_account_id` (7+ digits)
- URLs are prefixed: `/{account_id}/boards/...`
- Middleware (`AccountSlug::Extractor`) extracts the account ID from the URL and sets `Current.account`
- The slug is moved from `PATH_INFO` to `SCRIPT_NAME`, making Rails think it's "mounted" at that path
- All models include `account_id` for data isolation
- Background jobs automatically serialize and restore account context
**Key insight**: This architecture allows multi-tenancy without subdomains or separate databases, making local development and testing simpler.
### Authentication & Authorization
**Passwordless magic link authentication**:
- Global `Identity` (email-based) can have `Users` in multiple Accounts
- Users belong to an Account and have roles: admin, member, system
- Sessions managed via signed cookies
- Board-level access control via `Access` records
### Core Domain Models
**Account** → The tenant/organization
- Has users, boards, cards, tags, webhooks
- Has entropy configuration for auto-postponement
**Identity** → Global user (email)
- Can have Users in multiple Accounts
- Session management tied to Identity
**User** → Account membership
- Belongs to Account and Identity
- Has role (admin/member/system)
- Board access via explicit `Access` records
**Board** → Primary organizational unit
- Has columns for workflow stages
- Can be "all access" or selective
- Can be published publicly with shareable key
**Card** → Main work item (task/issue)
- Sequential number within each Account
- Rich text description and attachments
- Lifecycle: triage → columns → closed/not_now
- Automatically postpones after inactivity ("entropy")
**Event** → Records all significant actions
- Polymorphic association to changed object
- Drives activity timeline, notifications, webhooks
- Has JSON `particulars` for action-specific data
### Entropy System
Cards automatically "postpone" (move to "not now") after inactivity:
- Account-level default entropy period
- Board-level entropy override
- Prevents endless todo lists from accumulating
- Configurable via Account/Board settings
### UUID Primary Keys
All tables use UUIDs (UUIDv7 format, base36-encoded as 25-char strings):
- Custom fixture UUID generation maintains deterministic ordering for tests
- Fixtures are always "older" than runtime records
- `.first`/`.last` work correctly in tests
### Background Jobs (Solid Queue)
Database-backed job queue (no Redis):
- Custom `FizzyActiveJobExtensions` prepended to ActiveJob
- Jobs automatically capture/restore `Current.account`
- Mission Control::Jobs for monitoring
Key recurring tasks (via `config/recurring.yml`):
- Deliver bundled notifications (every 30 min)
- Auto-postpone stale cards (hourly)
- Cleanup jobs for expired links, deliveries
### Sharded Full-Text Search
16-shard MySQL full-text search instead of Elasticsearch:
- Shards determined by account ID hash (CRC32)
- Search records denormalized for performance
- Models in `app/models/search/`
## Coding style
Please read the separate file `STYLE.md` for some guidance on coding style.
+1 -1
View File
@@ -14,7 +14,7 @@ And then run the development server:
bin/dev
You'll be able to access the app in development at http://development-tenant.fizzy.localhost:3006
You'll be able to access the app in development at http://fizzy.localhost:3006
## Running tests
+217
View File
@@ -0,0 +1,217 @@
# Style
We aim to write code that is a pleasure to read, and we have a lot of opinions about how to do it well. Writing great code is an essential part of our programming culture, and we deliberately set a high bar for every code change anyone contributes. We care about how code reads, how code looks, and how code makes you feel when you read it.
We love discussing code. If you have questions about how to write something, or if you detect some smell you are not quite sure how to solve, please ask away to other programmers. A Pull Request is a great way to do this.
When writing new code, unless you are very familiar with our approach, try to find similar code elsewhere to look for inspiration.
## Conditional returns
In general, we prefer to use expanded conditionals over guard clauses.
```ruby
# Bad
def todos_for_new_group
ids = params.require(:todolist)[:todo_ids]
return [] unless ids
@bucket.recordings.todos.find(ids.split(","))
end
# Good
def todos_for_new_group
if ids = params.require(:todolist)[:todo_ids]
@bucket.recordings.todos.find(ids.split(","))
else
[]
end
end
```
This is because guard clauses can be hard to read, especially when they are nested.
As an exception, we sometimes use guard clauses to return early from a method:
* When the return is right at the beginning of the method.
* When the main method body is not trivial and involves several lines of code.
```ruby
def after_recorded_as_commit(recording)
return if recording.parent.was_created?
if recording.was_created?
broadcast_new_column(recording)
else
broadcast_column_change(recording)
end
end
```
## Methods ordering
We order methods in classes in the following order:
1. `class` methods
2. `public` methods with `initialize` at the top.
3. `private` methods
## Invocation order
We order methods vertically based on their invocation order. This helps us to understand the flow of the code.
```ruby
class SomeClass
def some_method
method_1
method_2
end
private
def method_1
method_1_1
method_1_2
end
def method_1_1
# ...
end
def method_1_2
# ...
end
def method_2
method_2_1
method_2_2
end
def method_2_1
# ...
end
def method_2_2
# ...
end
end
```
## To bang or not to bang
Should I call a method `do_something` or `do_something!`?
As a general rule, we only use `!` for methods that have a correspondent counterpart without `!`. In particular, we dont use `!` to flag destructive actions. There are plenty of destructive methods in Ruby and Rails that do not end with `!`.
## Visibility modifiers
We don't add a newline under visibility modifiers, and we indent the content under them.
```ruby
class SomeClass
def some_method
# ...
end
private
def some_private_method_1
# ...
end
def some_private_method_2
# ...
end
end
```
If a module only has private methods, we mark it `private` at the top and add an extra new line after but don't indent.
```ruby
class SomeModule
private
def some_private_method
# ...
end
end
```
## CRUD operations from controllers
In general, we favor a vanilla Rails approach to CRUD operations. We create and update models from Rails controllers passing the parameters directly to the model constructor or update method. We do not use services or form objects to handle these operations.
There are exceptional scenarios where we need to perform more complex operations, and we use form objects or higher-level service methods to handle them. We use the same pattern for both creations and updates.
Related to this, we prefer to avoid [nested attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html). If you find yourself wanting to use `accepts_nested_attributes_for`, that's a good smell that you might want to consider using a form object instead.
As an example, you can check how we create and update messages in HEY's: `MessagesController`:
```ruby
class MessagesController < ApplicationController
def create
@entry = Entry.enter \
new_message,
on: new_topic,
status: :drafted,
address: entry_addressed_param,
scheduled_delivery_at: entry_scheduled_delivery_at_param,
scheduled_bubble_up_on: entry_scheduled_bubble_up_on_param
respond_to_saved_entry @entry
end
def update
previously_scheduled = @entry.scheduled_delivery
@entry.revise \
message_params,
status: :drafted,
is_delivery_imminent: !entry_status_param.drafted?,
address: entry_addressed_param,
scheduled_delivery_at: entry_scheduled_delivery_at_param,
scheduled_bubble_up_on: entry_scheduled_bubble_up_on_param
respond_to_saved_entry(@entry, previously_scheduled: previously_scheduled)
end
end
class Entry < ApplicationRecord
def self.enter(*args, **kwargs)
Entry::Enter.new(*args, **kwargs).perform
end
def revise(*args, **kwargs)
Entry::Revise.new(self, *args, **kwargs).perform
end
end
```
## Run async operations in jobs
As a general rule, we write shallow job classes that delegate the logic itself to domain models:
* We typically use the suffix `_later` to flag methods that enqueue a job.
* A common scenario is having a model class that enqueues a job that, when executed, invokes some method in that same class. In this case, we use the suffix `_now` for the regular synchronous method.
```ruby
module Event::Relaying
extend ActiveSupport::Concern
included do
after_create_commit :relay_later
end
def relay_later
Event::RelayJob.perform_later(self)
end
def relay_now
# ...
end
end
class Event::RelayJob < ApplicationJob
def perform(event)
event.relay_now
end
end
```