calebporzio/bear-sync
Bear Sync is a Laravel package that imports your Bear notes into your application’s database and keeps them in sync. It provides Eloquent models and Artisan commands to pull notes, tags, and attachments from Bear so you can build on your notes.
Architecture fit. calebporzio/bear-sync is a read-only ingestion adapter, and it should be evaluated as one. It opens Bear's local SQLite store (~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite), maps notes, tags, attachments, and metadata into Eloquent models, and ships an Artisan sync command. That is the whole surface area. Architecturally this is the right shape: notes flow in as an edge, the application owns everything downstream — storage, search, publishing, permissions. The package should never be treated as a domain layer or a system of record. Its models are an ingestion artifact; if the app binds directly to the package namespace, the exit cost goes from one mapping job to a refactor.
Integration feasibility. Feasible, but not as a dependency. The repo is archived, the last release (v1.2.0, Feb 2020) added Laravel 7 support, and it declares PHP 7.x-era constraints. On a Laravel 10/11 or PHP 8.x app, composer require will fail resolution or pull in stale framework glue. There is no supported install path on a modern stack. The realistic work is one of two things, both measured in days:
src/ and config/, lift the schema-mapping queries into an app-owned BearReader (~150 lines), and own them.The environmental constraint matters more than the code: Bear is Apple-only, file-system-local, with no cloud API and no webhooks. That caps the addressable audience of anything built on this to internal tools, single-author products, and personal knowledge bases. A Mac-side sync agent would be required for any Linux or serverless topology — itself a product decision, because it caps the audience further.
Technical risk. Ranked:
[] and the sync "succeeds." Downstream interprets that as "user has no notes." This is the nastiest failure mode because it is invisible.migrate:fresh runs.Key questions.
Bottom line. The value here is a reference implementation and a cheap feasibility gate — a readable, ~150-line answer to "can Bear notes feed a Laravel app?" — not a runtime dependency. Adopt the pattern, vendor or rewrite the code, keep the fork small enough to rewrite from scratch in a day.
Stack fit. The entire value proposition is Eloquent ergonomics plus an Artisan command. On a Laravel-native team already comfortable with Eloquent relations, the code reads in an afternoon. On a non-Laravel team, a 50-line SQLite reader in any language is equivalent and carries far less baggage — so this is a Laravel-shop-only proposition.
Migration path. Two supported paths, and the decision should be made explicitly rather than by default:
src/ and config/, extract the schema-mapping queries into app/Support/BearSync/BearReader.php, own it. Roughly 150 lines. Best exit economics, no dependency surface, no constraint fights.app/Support/BearSync or a private namespace, rewrite composer.json constraints for PHP 8 / Laravel 10+, replace the service provider binding, namespace and timestamp the migrations, and keep migrations in a separate path so migrate:fresh on the app doesn't build Bear tables it doesn't own.Either way, do not add it to composer.json on a stack we care about. There is no version of this that should be a managed dependency in 2024+.
Compatibility. Only Laravel 6/7 with PHP 7.2–7.4 works as shipped. Modern stacks require: constraint rewrite, PHP 8 testing (typed properties, deprecations, string/array function changes), and verification that the Eloquent model layer still matches current conventions. The migrations need timestamp prefixes and a separate load path. If we stay on Laravel 6/7 for other reasons, the normal install is publish-config, run migrations, run bear:sync — but that is a very narrow window.
Sequencing. The order matters because each step de-risks the next:
storage/app/bear/ first — this removes lock contention and gives a stable snapshot. Configure at the copy and verify note bodies, tag assignments, and at least one note with multiple images. Multi-image is the sharpest canary for schema drift because it is the one path with a known historical defect.BearReader interface exposing notes(), tags(), attachments() and nothing else. Bind the implementation in a small service provider. Consumers never touch the package namespace, so replacing the source later changes one binding.withoutOverlapping(), onOneServer(), and runInBackground(). On a single Mac mini withoutOverlapping is the only concurrency control actually needed, but add it — a hung sync holding the SQLite handle poisons every subsequent run.posts/articles with updateOrCreate keyed on source ID. Render Markdown to HTML at ingest, not at read: one-time cost, simpler search index, and broken image references surface in the job where they can be logged rather than in a Blade view at 2 a.m.#projects/blog/draft) and the package flattens them to strings. Build the tree in our layer and decide up front whether #projects matches #projects/blog.Maintenance. No upstream maintenance, by definition. This is an internal fork once we touch it, and the budget question is not "when will the vendor patch this" but "what is our annual cost to keep it working." Expect a revalidation task on every Bear major release and every macOS release that could relocate the group container. The group container path includes spaces and a versioned UUID; quote it everywhere, expand $HOME in config rather than hardcoding, and write the error message so a relocation reads as a config change rather than a bug. Budget realistically: a few hours per year in steady state, plus whatever the next schema break costs.
Support. There is no vendor contract, no community, no security patching, and no compatibility guarantee. Support is entirely internal, which means the ownership question is the real operational risk. An unmaintained ingestion edge becomes nobody's job by default. Name an owner for schema revalidation and for responding to the zero-row alert; write it down, because the code will outlive the conversation that produced it.
Scaling. This scales to exactly one Mac mini and one tenant. SQLite on a local file, a single sync process, a single machine — horizontal scaling is not applicable. More authors or more Macs means more agents, each requiring its own operational story, which is a product decision as much as an infrastructure one. Note corpus size is not the constraint; SQLite handles tens of thousands of notes comfortably. Search is the thing to scale deliberately: don't lean on Bear's undocumented full-text tables — they're rebuilt by Bear and will drift first. Re-index note bodies into our own store (Scout, Meilisearch, Postgres FTS) during ingest. A few lines, and it removes an entire class of failure.
Failure modes and mitigations.
| Failure | Symptom | Mitigation |
|---|---|---|
| Live DB lock / WAL contention | Locked-database exceptions, truncated snapshots | Always copy the DB before syncing; never point at the live file while Bear is open |
| iCloud partial replication | File present but incomplete | open() + PRAGMA quick_check before sync; bail with a clear message |
| Schema drift | Silent empty query results, "successful" sync | Schema guard in a provider or health command; zero-row / >30% delta alert |
| Missing or re-encoded image | Exception mid-batch, lost remainder | Degrade gracefully — placeholder + logged warning — rather than throw |
| Encrypted notes | Ciphertext rendered as garbage in a view | Explicit skip/flag/placeholder decision before it reaches a template |
| Duplicate migrations | migrate:fresh tries to build Bear tables |
Separate migration path, namespaced, timestamped |
| Hung sync | Handle held, subsequent runs poisoned | withoutOverlapping(), onOneServer(), background run |
Treat zero rows as a failure, not an empty state. That single rule covers the most expensive failure mode in this integration.
Ramp-up. Small and fast. A competent Laravel engineer reads the package in an afternoon and can own a rewrite in one to two days. The skill set is ordinary — Eloquent, SQLite basics, a scheduler entry — with no specialized knowledge required. The work that actually takes time is not the reader; it's the guard, the alert, the fixture suite, and the downstream mapping job. Plan for roughly a week of engineering end to end to have something we'd stand behind internally, and treat the package itself as one day of that.
How can I help you explore Laravel packages today?