Hexagonal (ports and adapters) architecture for clean separation of concerns.
Quick Links: README | Commands | Development
cmd/nylas/ # Entry point (main.go)
internal/
domain/ # Business entities (28 files)
ports/ # Interface contracts (7 files)
adapters/ # Implementations
nylas/ # Nylas API client (94 files)
ai/ # AI providers (Claude, OpenAI, Groq, Ollama)
analytics/ # Focus optimizer, meeting scorer
keyring/ # Secret storage
config/ # Configuration validation
mcp/ # MCP proxy server
slack/ # Slack API client
utilities/ # Timezone, scheduling, contacts services
oauth/ # OAuth callback server
browser/ # Browser automation
tunnel/ # Cloudflare tunnel
webhookserver/ # Webhook server
cli/ # CLI commands
common/ # Shared helpers (client, context, errors, flags, format, html, timeutil)
admin/ # API key management
ai/ # AI commands
auth/ # Authentication
calendar/ # Calendar & events
contacts/ # Contact management
email/ # Email operations
inbound/ # Inbound email rules
integration/ # CLI integration tests
mcp/ # MCP server command
notetaker/ # Meeting notetaker
otp/ # OTP extraction
scheduler/ # Booking pages
setup/ # First-time setup wizard (nylas init)
slack/ # Slack integration
timezone/ # Timezone utilities
update/ # Self-update
webhook/ # Webhook management
ui/ # Web UI (port 7363)
air/ # Web email client (port 7365)
tui/ # Terminal UI
app/ # Shared app logic (auth, otp)
testutil/ # Test utilities
util/ # General utilities
docs/ # Documentation
commands/ # Command guides (12 files)
ai/ # AI docs (8 files)
security/ # Security docs (2 files)
troubleshooting/ # Troubleshooting (5 files)
development/ # Dev guides (4 files)
.claude/ # Claude configuration
commands/ # Skills/commands
agents/ # Agent definitions
rules/ # Project rules
shared/patterns/ # Code patterns
| Looking for | Location |
|---|---|
| CLI | |
| CLI entry & registration | cmd/nylas/main.go |
| CLI helpers (context, config) | internal/cli/common/ |
| CLI integration tests | internal/cli/integration/ |
| Adapters | |
| Nylas HTTP client | internal/adapters/nylas/client.go |
| AI providers | internal/adapters/ai/ |
| MCP server | internal/adapters/mcp/ |
| Slack adapter | internal/adapters/slack/ |
| Timezone service | internal/adapters/utilities/timezone/ |
| User Interfaces | |
| Air web client (port 7365) | internal/air/ |
| UI config tool (port 7363) | internal/ui/ |
| TUI terminal client | internal/tui/ |
| Tests | |
| CLI integration tests | internal/cli/integration/*_test.go |
| Air integration tests | internal/air/integration_*_test.go |
| Test utilities | internal/testutil/ |
| Docs | |
| Documentation index | docs/INDEX.md |
| Command reference | docs/COMMANDS.md |
| Claude rules | .claude/rules/ |
| Layer | Location | Purpose |
|---|---|---|
| App Services | internal/app/ |
Orchestrates adapters for workflows (auth login, OTP extraction) |
| CLI Helpers | internal/cli/common/ |
Reusable utilities (context, format, colors, pagination) |
| Adapter Helpers | internal/adapters/nylas/client_helpers.go |
HTTP helpers, request building, response handling |
| Air Helpers | internal/air/handlers_helpers.go |
Handler utilities (config checks, JSON parsing, demo mode) |
Key difference: App services coordinate multiple adapters. CLI helpers are stateless utilities. Adapter helpers handle API specifics.
| File | Helpers | Purpose |
|---|---|---|
client.go |
GetNylasClient(), GetCachedNylasClient(), ResetCachedClient(), GetAPIKey(), GetGrantID() |
Nylas client creation and credential access |
colors.go |
Bold, BoldWhite, Dim, Cyan, Green, Yellow, Red, Blue |
Shared color definitions |
config.go |
GetConfigStore(), GetConfigPath() |
Config store access from commands |
context.go |
CreateContext(), CreateContextWithTimeout() |
Context creation with timeouts |
errors.go |
WrapError(), FormatError(), PrintFormattedError(), NewUserError(), NewInputError(), WrapGetError(), WrapFetchError(), WrapCreateError(), WrapUpdateError(), WrapDeleteError(), WrapSendError() |
Error wrapping and formatting |
flags.go |
AddLimitFlag(), AddFormatFlag(), AddIDFlag(), AddPageTokenFlag(), AddForceFlag(), AddYesFlag(), AddVerboseFlag() |
Common CLI flag definitions |
format.go |
ParseFormat(), NewFormatter(), NewTable(), FormatParticipant(), FormatParticipants(), FormatSize(), PrintEmptyState(), PrintEmptyStateWithHint(), PrintListHeader(), PrintSuccess(), PrintError(), PrintWarning(), PrintInfo(), Confirm() |
Display formatting and output helpers |
html.go |
StripHTML(), RemoveTagWithContent() |
HTML-to-text conversion |
logger.go |
InitLogger(), GetLogger(), IsDebug(), IsQuiet(), Debug(), Info(), Warn(), Error(), DebugHTTP(), DebugAPI() |
Logging utilities |
pagination.go |
FetchAllPages(), FetchAllWithProgress(), NewPaginatedDisplay(), PageResult[T] |
Pagination helpers |
path.go |
ValidateExecutablePath(), FindExecutableInPath(), SafeCommand() |
Safe executable path handling |
progress.go |
NewSpinner(), NewProgressBar(), NewCounter() |
Progress indicators |
retry.go |
WithRetry(), DefaultRetryConfig(), NoRetryConfig(), IsRetryable(), IsRetryableStatusCode() |
Retry logic with backoff |
string.go |
Truncate() |
String utilities |
time.go |
FormatTimeAgo(), ParseTimeOfDay(), ParseTimeOfDayInLocation(), ParseDuration() |
Time formatting and parsing |
timeutil.go |
ParseDate(), ParseTime(), FormatDate(), FormatDisplayDate() + constants |
Date parsing and formatting |
| Helper | Purpose |
|---|---|
doGet(ctx, url, &result) |
GET request with JSON decoding |
doGetWithNotFound(ctx, url, &result, notFoundErr) |
GET with 404 handling |
doDelete(ctx, url) |
DELETE request (accepts 200/204) |
ListResponse[T] |
Generic paginated response type |
QueryBuilder |
Fluent URL query parameter builder |
QueryBuilder methods:
NewQueryBuilder()- Create new builderAdd(key, value)- Add string value (if non-empty)AddInt(key, value)- Add int value (if > 0)AddInt64(key, value)- Add int64 value (if > 0)AddBool(key, value)- Add bool value (if true)AddBoolPtr(key, value)- Add bool pointer (if non-nil)AddSlice(key, values)- Add multiple values with same keyEncode()- Get encoded query stringValues()- Get underlying url.ValuesBuildURL(baseURL)- Append query to URL
QueryBuilder usage:
qb := NewQueryBuilder().
Add("limit", "50").
AddInt("offset", params.Offset).
AddBoolPtr("unread", params.Unread)
url := qb.BuildURL(baseURL)Three layers:
-
Domain (
internal/domain/) - 29 files- Pure business logic, no external dependencies
- Core types: Message, Email, Calendar, Event, Contact, Grant, Webhook
- Feature types: AI, Analytics, Admin, Scheduler, Notetaker, Slack, Inbound
- Support types: Config, Errors, Provider, Utilities
- Shared interfaces:
interfaces.go(Paginated, QueryParams, Resource, Timestamped, Validator)
Key type relationships:
Person- Base type with Name/Email (incalendar.go)EmailParticipant- Type alias forPerson(inemail.go)Participant- EmbedsPerson, adds Status/Comment for calendar events
-
Ports (
internal/ports/) - 7 interface filesnylas.go- NylasClient interface (main API operations)secrets.go- SecretStore interface (credential storage)llm.go- LLM interface (AI providers)slack.go- Slack interfaceconfig.go- Config interfaceutilities.go- Utilities interfacewebhook_server.go- Webhook server interface
-
Adapters (
internal/adapters/) - 12 adapter directoriesAdapter Files Purpose nylas/94 Nylas API client (messages, calendars, contacts, events) ai/24 AI clients (Claude, OpenAI, Groq, Ollama), email analyzer analytics/14 Focus optimizer, conflict resolver, meeting scorer keyring/6 Credential storage (system keyring, file-based) mcp/8 MCP proxy server for AI assistants slack/21 Slack API client (channels, messages, users) config/5 Configuration validation oauth/3 OAuth callback server utilities/12 Services (contacts, email, scheduling, timezone, webhook) browser/2 Browser automation tunnel/2 Cloudflare tunnel webhookserver/2 Webhook server
Benefits:
- Testability (mock adapters)
- Flexibility (swap implementations)
- Clean separation of concerns
Calendar enforces working hours (soft warnings) and break blocks (hard constraints).
Domain models:
WorkingHoursConfig- Per-day working hours with break periodsDaySchedule- Working hours for specific weekdayBreakBlock- Break periods (lunch, coffee) with hard constraints
Configuration: ~/.nylas/config.yaml
Implementation: internal/cli/calendar/helpers.go (checkBreakViolation())
Tests: internal/cli/calendar/helpers_test.go
Details: See commands/timezone.md
Shared interfaces in internal/domain/interfaces.go enable generic programming:
| Interface | Purpose | Implemented By |
|---|---|---|
Paginated |
Resources with pagination info | MessageListResponse, EventListResponse, etc. |
QueryParams |
Query parameter types | MessageQueryParams, EventQueryParams, etc. |
Resource |
Resources with ID and GrantID | Message, Event, Contact, etc. |
Timestamped |
Resources with timestamps | Message, Event, Draft, etc. |
Validator |
Self-validating types | EventWhen, SendMessageRequest, BreakBlock |
Type embedding example:
// Person is the base type for email/calendar participants
type Person struct {
Name string `json:"name,omitempty"`
Email string `json:"email"`
}
// Participant embeds Person and adds calendar-specific fields
type Participant struct {
Person
Status string `json:"status,omitempty"`
Comment string `json:"comment,omitempty"`
}
// EmailParticipant is an alias for Person (backward compatibility)
type EmailParticipant = PersonEach feature follows consistent structure:
internal/cli/<feature>/
├── <feature>.go # Main command
├── list.go # List subcommand
├── create.go # Create subcommand
├── update.go # Update subcommand
├── delete.go # Delete subcommand
└── helpers.go # Shared helpers
The CLI provides three different interfaces:
| Interface | Command | Port | Purpose | Location |
|---|---|---|---|---|
| TUI | nylas tui |
N/A | Terminal-based email/calendar client | internal/tui/ |
| UI | nylas ui |
7363 | Web-based CLI configuration tool | internal/ui/ |
| Air | nylas air |
7365 | Full web-based email/calendar client | internal/air/ |
Which to use? TUI for terminal lovers, Air for browser-based email client, UI for API credential setup.
Air is a full-featured web-based email client for Nylas CLI, providing browser interface for email, calendar, and productivity features.
- Location:
internal/air/ - Server: HTTP server with middleware stack (CORS, compression, security, caching)
- Handlers: Feature-specific HTTP handlers (email, calendar, contacts, AI)
- Templates: Go templates with Tailwind CSS
- Port: Default
:7365(configurable)
All files are ≤500 lines for maintainability. Large files have been refactored into focused modules:
Server Core (refactored from server.go):
server.go- Server struct definitionserver_lifecycle.go- Initialization, routing, lifecycleserver_stores.go- Cache store accessorsserver_sync.go- Background sync logicserver_offline.go- Offline queue processingserver_converters.go- Domain to cache conversionsserver_template.go- Template handlingserver_modules_test.go- Unit tests
Handler Helpers:
handlers_helpers.go- Common handler utilities (see pattern below)
Handlers (organized by feature):
- Email:
handlers_email.go,handlers_drafts.go,handlers_bundles.go - Calendar:
handlers_calendars.go,handlers_events.go,handlers_calendar_helpers.go - Contacts:
handlers_contacts.go,handlers_contacts_crud.go,handlers_contacts_search.go,handlers_contacts_helpers.go - AI:
handlers_ai_types.go,handlers_ai_summarize.go,handlers_ai_smart.go,handlers_ai_thread.go,handlers_ai_complete.go,handlers_ai_config.go - Productivity:
handlers_scheduled_send.go,handlers_undo_send.go,handlers_templates.go,handlers_snooze_*.go,handlers_splitinbox_*.go
Other:
middleware.go- Middleware stackdata.go- Data modelstemplates/- HTML templatesintegration_*.go- Integration tests (organized by feature)
All HTTP handlers use common helpers for consistency and reduced boilerplate:
| Helper | Location | Purpose |
|---|---|---|
withTimeout(r) |
handlers_helpers.go |
Creates context with 30s default timeout |
requireConfig(w) |
handlers_helpers.go |
Checks Nylas client is configured, writes error if not |
parseJSONBody[T](w, r, &dest) |
handlers_helpers.go |
Generic JSON body parsing with error handling |
handleDemoMode(w, data) |
handlers_helpers.go |
Returns demo response if in demo mode |
requireMethod(w, r, method) |
handlers_helpers.go |
Validates HTTP method |
writeError(w, status, msg) |
handlers_helpers.go |
Writes JSON error response |
requireDefaultGrant(w) |
server_stores.go |
Gets default grant ID, writes error if not set |
getEmailStore(email) |
server_stores.go |
Gets email cache store for account |
getEventStore(email) |
server_stores.go |
Gets event cache store for account |
getContactStore(email) |
server_stores.go |
Gets contact cache store for account |
getFolderStore(email) |
server_stores.go |
Gets folder cache store for account |
getSyncStore(email) |
server_stores.go |
Gets sync cache store for account |
Standard handler pattern:
func (s *Server) handleX(w http.ResponseWriter, r *http.Request) {
if s.handleDemoMode(w, demoData) { return }
if !s.requireConfig(w) { return }
grantID, ok := s.requireDefaultGrant(w)
if !ok { return }
ctx, cancel := s.withTimeout(r)
defer cancel()
// ... handler logic
}Complete file listing: See CLAUDE.md for detailed file structure with line counts
Air integration tests are split by feature for better maintainability:
| File | Tests | Purpose |
|---|---|---|
integration_base_test.go |
0 | Shared testServer() helper, utilities, rate limiting |
integration_core_test.go |
5 | Config, Grants, Folders, Index page |
integration_email_test.go |
4 | Email listing, filtering, drafts |
integration_calendar_test.go |
11 | Calendars, events, availability, conflicts |
integration_contacts_test.go |
4 | Contact CRUD operations |
integration_cache_test.go |
4 | Cache store operations, invalidation |
integration_ai_test.go |
15 | AI summarization, smart compose, thread analysis, config |
integration_middleware_test.go |
6 | Compression, security headers, CORS |
integration_bundles_test.go |
8 | Email bundles, categorization, bundle operations |
integration_productivity_test.go |
8 | Scheduled send, undo send, snooze, reply later |
Total: 65 integration tests across 10 organized files
Running tests:
make ci-full # RECOMMENDED: Complete CI with automatic cleanup
make test-air-integration # Run Air integration tests only
make test-cleanup # Manual cleanup if neededWhy cleanup? Air tests create real resources (drafts, events, contacts) in the connected Nylas account. The make ci-full target automatically runs cleanup after all tests.
Pattern: Air tests use httptest to test HTTP handlers directly:
func TestIntegration_Feature(t *testing.T) {
server := testServer(t) // Shared helper
req := httptest.NewRequest(http.MethodGet, "/api/endpoint", nil)
w := httptest.NewRecorder()
server.handleEndpoint(w, req)
// Assertions...
}For detailed implementation, see CLAUDE.md and docs/DEVELOPMENT.md