Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bear Sync Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Understand what this is before you install it. calebporzio/bear-sync is a read-only ingestion adapter, not a notes platform. It opens Bear's local SQLite database — ~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite — and mirrors notes, tags, and attachments into your own database tables as Eloquent models. It ships one Artisan command to re-run the sync. That's the entire surface area, and it's why the first thing you should do is read src/ and config/ rather than reaching for composer require.

Decide your path in the first hour. The repo is archived, the last release is February 2020 (Laravel 7), and it has zero dependents. On a modern Laravel app, dependency resolution will fail or drag in stale framework glue. Pick one:

  • Vendor/fork path (recommended): copy the package into app/Support/BearSync or a private namespace, bump constraints, and register your own service provider binding. You get the migrations and Eloquent models as a starting point and own them outright.
  • Reference path: read the schema-mapping queries and lift them into ~150 lines of app-owned BearReader code. Cheapest exit.
  • Straight install only makes sense if you're genuinely on Laravel 6/7 with PHP 7.2–7.4.

If you do install normally:

composer require calebporzio/bear-sync
php artisan vendor:publish --provider="CalebPorzio\BearSync\BearSyncServiceProvider"
php artisan migrate
php artisan list | grep bear   # confirm the command signature
php artisan bear:sync

Copy the database first, point the config at the copy. This is not optional and it's the whole setup:

mkdir -p storage/app/bear
cp "$HOME/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite" \
   storage/app/bear/database.sqlite

Then set the config to that path, run the sync, and verify end-to-end in Tinker:

BearNote::count();                                 // non-zero?
BearNote::first()->title;
BearNote::with('tags')->limit(5)->get();           // tag relation wired?

Your first use-case should be a canary, not a feature. The single highest-value thing you can confirm on day one is that note bodies, tag assignments, and a note with multiple images all come through correctly. The multiple-image path is the only thing the final release fixed ("support for multiple image replacements was broken"), which makes it your sharpest signal for schema drift. If it works, you have a viable ingestion edge; if it doesn't, you've learned that in a day instead of a sprint.

Implementation Patterns

Treat the package as an edge, not a domain layer. Keep the imported Bear tables raw and untouched. Add your own posts/articles table downstream and normalize from Bear into it inside a job or command. Every consumer of your app reads your table, never the package's models. If Bear disappears tomorrow, you rewrite one mapping job.

// app/Console/Commands/ImportBearNotes.php
public function handle(BearReader $reader, MarkdownConverter $md): void
{
    foreach ($reader->notes() as $note) {
        Post::updateOrCreate(
            ['source_id' => $note->id],
            [
                'title'     => $note->title,
                'body'      => $md->convert($note->text),
                'slug'      => Str::slug($note->title),
                'status'    => $note->trashed ? 'archived' : 'draft',
                'synced_at' => now(),
            ],
        );
    }
}

Wrap it behind a one-method-face interface. Expose only notes(), tags(), and attachments() from a BearReader contract. Your app binds EloquentBearReader today, and swapping in an exported-Markdown reader later becomes a binder change, not a refactor.

$this->app->bind(BearReader::class, EloquentBearReader::class);

Build the tag tree yourself. Bear tags are hierarchical (#projects/blog/draft), but the package stores them as flat strings. Do not try to coerce the model into a tree — build one in your own layer, and decide up front whether #projects should match a lookup for #projects/blog. That decision ripples into search, navigation, and permissions, so make it deliberately rather than by accident.

Render Markdown at ingest time. Convert note bodies to HTML inside the sync job and store both the source text and the rendered HTML. A broken image reference is a logged warning during a scheduled job, not a fatal Blade error at 2 a.m. If you use a search engine (Scout/Meilisearch/Postgres FTS), index the rendered body during the same pass — do not query Bear's own full-text tables.

Schedule it with the usual hygiene. Even on a single Mac mini, one hung sync that holds a SQLite handle will poison every subsequent run.

$schedule->command('bear:sync')
    ->hourly()
    ->withoutOverlapping()
    ->onOneServer()
    ->runInBackground();

Write fixture tests against an anonymized real database. The point isn't coverage of the package — it's coverage of the schema guard. Include one note with multiple images, one note with an attachment whose file is missing, and one encrypted note. Those three fixtures catch the real defects.

public function test_sync_imports_multiple_images_per_note(): void
{
    $this->useBearFixture('note-with-multiple-images.sqlite');
    $this->artisan('bear:sync')->assertExitCode(0);
    $this->assertSame(3, Attachment::count());
}

Keep migrations in a separate path. If you fork, give the package's migrations their own namespace and timestamp so migrate:fresh on your app doesn't try to build tables it doesn't own.

Gotchas and Tips

Composer will fight you. Archived, Laravel-7-era constraints, no maintainer. Don't paper over it with --ignore-platform-reqs. Fork, vendor, or lift the logic — there is no scenario where this belongs in composer.json on a stack you care about.

Never sync against the live Bear database. SQLite WAL mode plus a UI holding write locks means occasional database is locked errors and, worse, snapshots taken mid-checkpoint that look valid but are truncated. Copy first. Always.

iCloud can hand you a truncated file. If Bear's store is replicating, the database can exist but be incomplete. Run a cheap check before every sync — open() plus PRAGMA quick_check — and fail with a clear, actionable error rather than importing half a corpus.

The path has spaces and a UUID. Quote it in shell commands, expand $HOME in config rather than hardcoding, and expect a macOS or Bear major version to relocate the container. That's a config change, not a bug — but it only looks that way if you wrote the error message to say so.

Schema drift is the number-one risk. The package reverse-engineers an undocumented CoreData schema; Shiny Frog can rename anything without notice. Wire a guard into a health-check command:

$tables = collect(DB::connection('bear')
    ->select("select name from sqlite_master where type='table'"))
    ->pluck('name');

if ($tables->doesntContain('ZSFNOTE')) {
    throw new RuntimeException('Bear schema changed — revalidate bear-sync before syncing.');
}

Treat zero rows as a failure, not an empty state. The nastiest failure is silent: a renamed column returns [], your sync reports success, and downstream you render "no posts." Alert when the row count is zero or swings more than ~30% between runs.

Encrypted notes come through as ciphertext. The package can't and won't decrypt them. Decide explicitly — skip, flag, or render a placeholder — before they land as garbage in a view.

Attachment resolution is the historically fragile path. v1.2.0's only fix was multiple image replacements, which tells you exactly where real defects live. Degrade gracefully when a referenced file is missing or re-encoded: placeholder image, logged warning, continue the batch. Never throw mid-sync and lose the rest.

Don't lean on Bear's FTS tables. They're undocumented, rebuilt by Bear, and will drift before anything else. Re-index into your own store during ingest — a few lines, one fewer class of failure.

Know your extension points before you need them. Realistically there are three: swap the DB path, replace the service-provider binding, wrap the models behind your interface. Anything deeper — new relations, different upsert semantics, per-note permissions — means forking. Since the repo is archived, forking is a one-way door; keep the fork small enough to rewrite from scratch in a day.

Name an owner. An unmaintained ingestion edge defaults to nobody's job. Write down who revalidates the schema on every Bear major release and who gets paged when the zero-row alert fires. This tip outlives the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package