Migrate your complete Basecamp workspace to structured, beautiful, fully linked Notion pages.
This Ruby-based CLI tool migrates full Basecamp data into Notion with high fidelity, including:
- β Projects
- β Messages + comments
- β Chats + subpages by year
- β Todosets
- β Vault docs
- β Schedules
- β Inbox forwards
- β Kanban boards
- β Questionnaires
- β Inline images, embeds, media (when external, not internal basecamp assets)
- β Metadata (authors, timestamps, source links)
- β Migration banners ("Migrated from Basecamp on DD/MM/YYYY β Source URL")
- β Smart sub-page splitting for large datasets
- β Progress tracking (projects, tools, individual items)
- β Safe to interrupt and resume β
- β Optional filters (per project label, per tool name)
The script is fully idempotent and checkpoint-resumable for large Basecamp accounts (multi-hour runs).
| Feature | Status |
|---|---|
| β Full Basecamp API coverage | Projects, dock tools, comments |
| β Rich Notion formatting | Headings, callouts, checklists, embeds, dividers |
| β Inline media support | Images, attachments, links, external embeds |
| β Progress tracking | Projects, tools, and items (messages, chats, todos...) |
| β Checkpoint resume | Safe re-runs from last progress |
| β Delta sync ready | Track last synced timestamp |
| β Split large tool pages | Automatically chunk by year / batch |
| β Migration banners | Add "Migrated from Basecamp" banner to every page |
| β Final sync report | Per-project, per-tool, per-item status |
| β SQLite progress database | Local file sync_progress.db for resume & audit |
| β Debug mode | Detailed logs and payload dumps in ./tmp/ |
- Ruby 3.2+
- Basecamp OAuth App credentials (client ID, client secret)
- Notion API integration token and page access
Install Ruby dependencies:
gem install sqlite3Basecamp no longer uses Personal Access Tokens.
Instead, create an OAuth app:
- Go to: Basecamp OAuth App Registration
- Register a new application:
- Name: Basecamp to Notion Migrator
- Redirect URI:
http://localhost:4567/callback
- After creation, copy:
- Client ID β
BASECAMP_CLIENT_ID - Client Secret β
BASECAMP_CLIENT_SECRET
- Client ID β
- Find your Basecamp Account ID:
- Visit: https://3.basecamp.com/
- Note the number in the URL:
https://3.basecamp.com/123456789/
- Go to: Notion My Integrations
- Create a new integration:
- Name: Basecamp to Notion Migrator
- Give full permissions:
- β Insert content
- β Read content
- β Update content
- Copy the Internal Integration Token β
NOTION_API_KEY - Share your target Notion page:
- Go to the Notion page you want to use as the root
- Click Share
- Add your integration to give it access
- Get your Notion Root Page ID:
- Copy link to your page
- Example:
https://www.notion.so/workspace/Your-Page-abcdef1234567890abcdef1234567890 - Use:
abcdef1234567890abcdef1234567890
Copy .env.sample to .env and fill in all values:
cp .env.sample .envFill in:
- β
BASECAMP_CLIENT_ID - β
BASECAMP_CLIENT_SECRET - β
BASECAMP_REDIRECT_URI - β
BASECAMP_ACCOUNT_ID - β
NOTION_API_KEY - β
NOTION_ROOT_PAGE_ID
Optional filters:
FILTER_PROJECT_LABEL(e.g., "Marketing HQ")FILTER_TOOL_NAME(e.g., "chat")
Once .env is prepared, run:
ruby basecamp_to_notion_via_api.rb-
β Creates Notion pages under your selected root
-
β Auto-generates migration banners with Basecamp links
-
β Logs progress and API payloads to
./tmp/ -
β Progress is saved in
sync_progress.db -
β Resume safely after interruption!
-
Include archived Basecamp projects:
INCLUDE_ARCHIVED=true ruby basecamp_to_notion_via_api.rb
You can narrow the scope of migration:
Project label filter: Only sync projects matching label (partial, case-insensitive).
FILTER_PROJECT_LABEL="Marketing HQ"Tool name filter: Only sync specific Basecamp tool types.
Valid values:
message_board, schedule, vault, chat, todoset, kanban_board, questionnaire, inbox
Example:
FILTER_TOOL_NAME="chat"β Filters are safe β progress tracker and resume flow fully supported.
- Local SQLite:
sync_progress.db - Tracks:
- β Projects
- β Tools
- β Items (messages, chats, todos...)
- Resume anytime: re-run the script!
Inspect manually:
sqlite3 sync_progress.dbFinal sync report:
β Written to sync_report.json at end of run.
If you want to perform a full fresh sync, starting from a clean state, you can enable RESET mode.
Set the environment variable:
RESET=true ruby basecamp_to_notion_via_api.rb- Notion limits nested ordered/unordered lists to a depth of 3.
- Anything seen deeper than this is shoved in the parent list as only option - otherwise Notion returns a 400.
- Basecamp's media files require authentication.
- They are not public.
- β API fetches them during sync, but Notion API currently does not support uploading.
- No file upload support (yet).
- β Rich text, embeds, links, and external media are supported.
- β External services (e.g., YouTube, Giphy) work well.
-
Each Notion page includes a yellow migration banner:
ποΈ Migrated from Basecamp on DD/MM/YYYY β π https://3.basecamp.com/...
-
For media:
- β External links are preserved.
- β Internal Basecamp assets include source link.
- β No broken placeholders.
Logs:
β API calls and Notion block payloads are saved to ./tmp/
Schema: β Auto-created, or run manually:
ruby database/schema.rbSQLite progress DB: β Inspect with:
sqlite3 sync_progress.dbDebug mode:
β Payload dumps in ./tmp/
β Safe to delete between runs.
- Fully tested end-to-end
- Multi-project and multi-hour runs safe
- Resume support: β
- Migration banners β
- Progress DB β
- Sync reports β
- Delta sync: sync only updated Basecamp content
- Parallelise projects (right now we parallelise tools per project)
- Media upload support (when Notion API allows)
Built by Tom Meier
Crafted for high-fidelity Basecamp β Notion migration. Production safe, multi-hour runs. Reliable. Beautiful output.
β Every Notion page includes:
βMigrated from Basecamp on DD/MM/YYYY β π Original Basecamp URLβ
β Progress is checkpointed and resumable.
β No media placeholders β fallback links ensure traceability.
If you get stuck:
- β
Check
tmp/for debug logs - β
Review
sync_progress.db - β Open any handler file to customize Notion formatting
Q: Will RESET=true delete my existing Notion pages?
A: No. RESET only clears local files and progress tracking. Existing Notion pages must be deleted manually if you want a fully clean start.
Q: Can I resume a sync if my internet drops?
A: Yes! The script is checkpointed and safe to resume. Simply run it again without RESET=true.
Q: Can I run with filters and RESET together? A: Yes. For example:
RESET=true FILTER_PROJECT_LABEL="My Project" ruby basecamp_to_notion_via_api.rb