<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Syed Ibrahim</title>
    <description>The latest articles on DEV Community by Syed Ibrahim (@syed11).</description>
    <link>https://dev.to/syed11</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3803808%2F50471324-3099-4572-bd07-3d5560280700.png</url>
      <title>DEV Community: Syed Ibrahim</title>
      <link>https://dev.to/syed11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syed11"/>
    <language>en</language>
    <item>
      <title>A Case Study in Integrating the Zoom Native SDK into a Flutter Monorepo</title>
      <dc:creator>Syed Ibrahim</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:40:07 +0000</pubDate>
      <link>https://dev.to/syed11/a-case-study-in-integrating-the-zoom-native-sdk-into-a-flutter-monorepo-4il</link>
      <guid>https://dev.to/syed11/a-case-study-in-integrating-the-zoom-native-sdk-into-a-flutter-monorepo-4il</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Forking a plugin, fighting Gradle and AAR packaging, migrating a repo to Git LFS, and chasing a SQLite ABI conflict that passed a green build — and what it took to keep all of it from leaking into the rest of the application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Integrating a third-party SDK into a Flutter application can look deceptively simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add package → Initialize SDK → Authenticate → Join meeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly what I expected when I started integrating the native Zoom Meeting SDK into my Flutter monorepo. It didn't turn out that way.&lt;/p&gt;

&lt;p&gt;The hard part wasn't calling Zoom's APIs from Dart. It was making a large native SDK coexist with an application that already had its own database layer, Android build configuration, package architecture, and repository constraints.&lt;/p&gt;

&lt;p&gt;By the end, the integration touched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forking an existing Flutter Zoom plugin and owning the native bridge myself&lt;/li&gt;
&lt;li&gt;Android namespace changes and Gradle AAR packaging restrictions&lt;/li&gt;
&lt;li&gt;Jetpack Compose / ViewBinding alignment&lt;/li&gt;
&lt;li&gt;280+ MB Android binaries and 145+ MB iOS binaries, and a Git LFS migration to hold them&lt;/li&gt;
&lt;li&gt;A duplicate &lt;code&gt;libsqlite3.so&lt;/code&gt; that turned into a genuine ABI compatibility bug&lt;/li&gt;
&lt;li&gt;Introducing a &lt;code&gt;MeetingService&lt;/code&gt; abstraction so my &lt;code&gt;courses&lt;/code&gt; domain package never has to know Zoom exists&lt;/li&gt;
&lt;li&gt;Making Zoom an optional, per-client dependency, since not every client on the platform needs video conferencing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important lesson wasn't about Zoom. It was about &lt;strong&gt;how to integrate a native SDK without letting the SDK dictate the architecture of the application around it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Starting Point
&lt;/h2&gt;

&lt;p&gt;My app is a Flutter monorepo with multiple packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cortex/
├── app/
├── packages/
│   ├── core/
│   ├── courses/
│   ├── exams/
│   ├── profile/
│   └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;courses&lt;/code&gt; package handles lessons, and, when appropriate, lets a student join a video conference. The obvious implementation would have been for &lt;code&gt;courses&lt;/code&gt; to talk to Zoom directly — but that immediately creates coupling: now the domain package knows about a specific vendor.&lt;/p&gt;

&lt;p&gt;I separated two concerns that are easy to conflate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application needs a way to join a meeting.&lt;/li&gt;
&lt;li&gt;Zoom is one implementation of that capability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That distinction shaped everything that followed.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Why I Forked the Plugin Instead of Using It As-Is
&lt;/h2&gt;

&lt;p&gt;I started with the &lt;code&gt;flutter_zoom_meeting_sdk&lt;/code&gt; package. It's a thin wrapper around Zoom's native Meeting SDK — its own dependencies are just &lt;code&gt;flutter&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, and &lt;code&gt;plugin_platform_interface&lt;/code&gt;. That's the plugin, not the SDK: it doesn't bundle any Zoom binaries. The documented setup is to download &lt;code&gt;mobilertc.aar&lt;/code&gt; yourself from the Zoom App Marketplace and manually copy it into the plugin's own installation folder inside &lt;code&gt;.pub-cache&lt;/code&gt; (&lt;code&gt;android/libs&lt;/code&gt;), then hand-edit that copy's Gradle file to add Zoom's required dependencies.&lt;/p&gt;

&lt;p&gt;That's workable for a quick prototype, but not for something I intended to keep working long-term. Anything placed by hand inside &lt;code&gt;.pub-cache&lt;/code&gt; disappears the moment someone runs &lt;code&gt;flutter pub cache clean&lt;/code&gt; or the plugin version changes — the build stops being reproducible from the repository itself, since the actual SDK binary and the Gradle edits it needs never get committed anywhere.&lt;/p&gt;

&lt;p&gt;On top of that, the plugin also pulled in &lt;code&gt;http&lt;/code&gt; for its own JWT-handling helpers, which overlapped with networking infrastructure I already had in the app. And since Zoom's SDK is large and native, I wanted direct control over its Android and iOS build configuration rather than treating it as an opaque pub.dev dependency I couldn't touch.&lt;/p&gt;

&lt;p&gt;So I created my own local package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/zoom/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and moved the native bridge into it — copying the plugin's Kotlin/Swift bridge code into my own package, and moving the SDK binaries (and the Gradle changes they require) into the repository instead of &lt;code&gt;.pub-cache&lt;/code&gt;. The goal wasn't to rewrite Zoom's SDK — it was to own the &lt;strong&gt;Flutter-to-Zoom bridge&lt;/strong&gt;, while still using Zoom's actual native binaries underneath. That package became the boundary between my application and Zoom, and let me drop the third-party plugin from my dependency tree entirely.&lt;/p&gt;

&lt;p&gt;I also took ownership of the Android namespace, changing it from the upstream plugin's &lt;code&gt;com.simitgroup&lt;/code&gt; to &lt;code&gt;com.testpress.flutter_zoom_meeting_sdk&lt;/code&gt;, updating package declarations, manifest entries, and Gradle config to match. Once you fork a native plugin, you inherit its entire native build identity — it's no longer "a package from pub.dev," it's part of your codebase.&lt;/p&gt;

&lt;p&gt;I also had to rework how the native bridge managed Flutter's platform-channel event sinks. The original implementation captured the &lt;code&gt;EventSink&lt;/code&gt; too early — before the Flutter engine had finished initializing — so events from Zoom sometimes arrived to a listener holding a stale null reference. The fix was to resolve the sink lazily via a closure at the moment it was actually needed, rather than caching it at construction time. It's a good reminder that a platform channel existing in code doesn't mean its runtime endpoint is ready the instant an object is constructed.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Android Build Problems: AARs Are Not Normal Dependencies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AARs don't drop in like a normal file
&lt;/h3&gt;

&lt;p&gt;The Zoom Android SDK ships as an AAR — &lt;code&gt;mobilertc.aar&lt;/code&gt;. My first instinct was to treat it like any local file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libs/mobilertc.aar"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android Gradle Plugin doesn't allow that when you're building another AAR yourself — the resulting library would be broken because the local AAR's classes and resources wouldn't be packaged correctly. The fix was to configure a local &lt;code&gt;flatDir&lt;/code&gt; repository and resolve it as a proper dependency instead of an arbitrary file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mobilertc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"aar"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The SDK raised the floor on the rest of the toolchain
&lt;/h3&gt;

&lt;p&gt;Zoom's SDK also came with its own minimum requirements for the rest of the Android build: &lt;code&gt;minSdk&lt;/code&gt; 26, NDK &lt;code&gt;27.0.12077973&lt;/code&gt;, Java 17 as both source and target compatibility, and core library desugaring enabled (&lt;code&gt;coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")&lt;/code&gt;). None of that is optional — it's what the native SDK needs to link and run, so I had to raise my app's own Android build to match.&lt;/p&gt;

&lt;h3&gt;
  
  
  The namespace change had downstream fallout
&lt;/h3&gt;

&lt;p&gt;The namespace change from earlier surfaced stale imports and references to native constants that didn't exist in my SDK version, and Zoom's native UI components required aligning ViewBinding and Jetpack Compose (via the Compose BOM) with the rest of the app's dependency graph. None of this was a Dart problem — it was the cost of bringing a large native Android SDK into a build that already had its own dependency graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Then GitHub Rejected the Repository
&lt;/h2&gt;

&lt;p&gt;Once the SDK was building locally, I hit a completely different wall: GitHub wouldn't accept the push. &lt;code&gt;mobilertc.aar&lt;/code&gt; was around 282 MB, and the iOS &lt;code&gt;MobileRTC&lt;/code&gt; binaries were around 145 MB — both well past GitHub's 100 MB per-file limit.&lt;/p&gt;

&lt;p&gt;Compression wasn't a real option. An AAR is already essentially a ZIP archive, so zipping it again doesn't meaningfully shrink 280 MB. I needed a system built for large binaries: &lt;strong&gt;Git LFS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of storing the full binary in the Git object database, Git LFS stores a small pointer and keeps the actual payload in LFS storage. Since the binaries were already committed to history, tracking them going forward wasn't enough — I had to migrate history itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs migrate import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.aar,MobileRTC"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include-ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;refs/heads/feat/zoom-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rewrote hundreds of commits on the feature branch, which then had to be force-pushed.&lt;/p&gt;

&lt;p&gt;Git LFS introduced its own gotcha shortly after: Gradle choked trying to process &lt;code&gt;mobilertc.aar&lt;/code&gt; when it was only ~134 bytes — a pointer file, not the actual archive, because the real LFS payload hadn't been pulled. The fix was making sure &lt;code&gt;.gitattributes&lt;/code&gt; correctly tracked the binaries and running &lt;code&gt;git lfs pull&lt;/code&gt; to materialize the real files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*.aar filter=lfs diff=lfs merge=lfs -text
MobileRTC filter=lfs diff=lfs merge=lfs -text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The underlying lesson: Git LFS is transparent to your build system only when the working tree actually contains the real binary. Without the LFS client, or without pulling the objects, your compiler sees the pointer, not the SDK.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Real Problem: Two &lt;code&gt;libsqlite3.so&lt;/code&gt; Files
&lt;/h2&gt;

&lt;p&gt;Once the SDK was compiling, I hit the most interesting failure of the whole project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution failed for task ':app:mergeDebugNativeLibs'
2 files found with path 'lib/armeabi-v7a/libsqlite3.so'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gradle pointed to two sources: &lt;code&gt;mobilertc&lt;/code&gt; and &lt;code&gt;sqlite3-native-library&lt;/code&gt;. I wasn't intentionally shipping two SQLite libraries — one came from Zoom, and the other turned out to come from my own database stack.&lt;/p&gt;

&lt;p&gt;I use Drift for local storage, and the dependency chain looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/core → drift → sqlite3 → native SQLite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail: newer versions of the &lt;code&gt;sqlite3&lt;/code&gt; package moved native SQLite compilation into Flutter's Native Assets system. So even without anyone writing &lt;code&gt;sqlite3_native_library&lt;/code&gt; anywhere, the dependency graph was quietly producing a &lt;code&gt;libsqlite3.so&lt;/code&gt; of its own.&lt;/p&gt;

&lt;p&gt;Here's the part that made this more than a packaging annoyance: &lt;strong&gt;two files with the same name aren't necessarily the same binary.&lt;/strong&gt; Zoom's native database layer expected specific SQLite symbols, and when the wrong implementation got packaged, the dynamic linker couldn't resolve one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.lang.UnsatisfiedLinkError:
dlopen failed:
cannot locate symbol "sqlite3_trace_v2"
referenced by "libZMDB.so"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The tempting, wrong fix
&lt;/h3&gt;

&lt;p&gt;Gradle offers a built-in escape hatch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;packaging&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;jniLibs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pickFirsts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"**/libsqlite3.so"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the build succeed — Gradle just picks one of the duplicate files and moves on. But &lt;code&gt;pickFirst&lt;/code&gt; only answers &lt;em&gt;which file gets packaged&lt;/em&gt;, not &lt;em&gt;which implementation is actually compatible with everything that loads it&lt;/em&gt;. In my case, the build went green and the app still crashed at runtime. A successful build is not the same thing as a correct native integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual fix
&lt;/h3&gt;

&lt;p&gt;Instead of overriding the packaging decision, I traced the dependency graph back to its source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;drift → sqlite3 3.x → Flutter Native Assets → libsqlite3.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I knew &lt;em&gt;why&lt;/em&gt; a second SQLite binary existed, the fix was straightforward — constrain the version so the newer Native Assets path never kicks in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drift&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^2.21.0&lt;/span&gt;
  &lt;span class="na"&gt;sqlite3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;3.0.0"&lt;/span&gt;
  &lt;span class="na"&gt;sqlite3_flutter_libs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.5.30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I kept &lt;code&gt;sqlite3_flutter_libs&lt;/code&gt; deliberately — it's not redundant with &lt;code&gt;sqlite3&lt;/code&gt;. &lt;code&gt;sqlite3&lt;/code&gt; provides the Dart-side FFI interface; &lt;code&gt;sqlite3_flutter_libs&lt;/code&gt; provides the native library that interface talks to on platforms where Zoom isn't involved. Removing it would have broken Drift on any build where Zoom's native SQLite wasn't present. The fix worked because it changed &lt;em&gt;what gets generated&lt;/em&gt;, not which of two already-generated binaries wins a coin flip.&lt;/p&gt;

&lt;p&gt;One thing worth being explicit about: this pairing is the &lt;em&gt;pre-3.0&lt;/em&gt; model. &lt;code&gt;sqlite3&lt;/code&gt;'s own upgrade guide recommends dropping &lt;code&gt;sqlite3_flutter_libs&lt;/code&gt; entirely once you move to &lt;code&gt;sqlite3: ^3.0.0&lt;/code&gt;, because 3.x switched to Flutter's hooks system to download and bundle SQLite automatically — &lt;code&gt;sqlite3_flutter_libs&lt;/code&gt; is a no-op from &lt;code&gt;0.6.0&lt;/code&gt; onward. I went the other way on purpose. Upgrading to 3.x wouldn't have solved my problem; it just moves &lt;em&gt;how&lt;/em&gt; the second &lt;code&gt;libsqlite3.so&lt;/code&gt; gets generated, from an old build script to a hook, while still generating one. Staying on &lt;code&gt;sqlite3 &amp;lt;3.0.0&lt;/code&gt; with the real &lt;code&gt;sqlite3_flutter_libs&lt;/code&gt; (&lt;code&gt;0.5.x&lt;/code&gt;, before it became a no-op) is what actually avoided producing a second native SQLite build in the first place.&lt;/p&gt;




&lt;p&gt;At this point the native integration problem was solved — Zoom initialized, authenticated, and joined meetings without crashing. But a different problem was still open: the integration &lt;em&gt;worked&lt;/em&gt;, yet I didn't want the rest of the application to know how it worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Separating Zoom from &lt;code&gt;courses&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;courses&lt;/code&gt; shouldn't know Zoom exists. So at the core layer I defined an abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MeetingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;joinMeeting&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;meetingNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;courses&lt;/code&gt; only depends on &lt;code&gt;MeetingService&lt;/code&gt; — it has no idea whether the implementation is Zoom, Teams, Meet, or nothing at all. I introduced a small registry in &lt;code&gt;packages/core&lt;/code&gt; so an implementation can be registered without core ever importing Zoom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/core
    ├── MeetingService
    ├── MeetingServiceRegistry
    └── meetingServiceProvider

packages/zoom
    └── ZoomMeetingService implements MeetingService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laid out in full, the composition looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌───────────────┐
                         │      app      │
                         │  Composition  │
                         │     Root      │
                         └───────┬───────┘
                                 │
                       registers implementation
                                 │
                                 ▼
┌──────────────┐        ┌─────────────────┐
│   courses    │───────▶│      core       │
│              │        │                 │
│  Conference  │        │ MeetingService  │
│      UI      │        │    Provider     │
└──────────────┘        └────────┬────────┘
                                  ▲
                                  │ implements
                                  │
                         ┌────────┴────────┐
                         │      zoom       │
                         │                 │
                         │ ZoomMeeting     │
                         │ Service         │
                         └────────┬────────┘
                                  │
                          Native Zoom SDK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;courses&lt;/code&gt; depends on a capability (&lt;code&gt;MeetingService&lt;/code&gt;), not on &lt;code&gt;zoom&lt;/code&gt; directly. &lt;code&gt;zoom&lt;/code&gt; implements that capability. &lt;code&gt;app&lt;/code&gt; is the only place that knows both exist, and it's the only place that wires them together.&lt;/p&gt;

&lt;p&gt;Since I already use Riverpod, it became the composition layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;meetingServiceProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MeetingService&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MeetingServiceRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;courses&lt;/code&gt; reads the provider and calls &lt;code&gt;joinMeeting()&lt;/code&gt; — it never imports &lt;code&gt;package:zoom&lt;/code&gt;. The lobby UI wraps this in a simple loading state (&lt;code&gt;joining = true/false&lt;/code&gt;) around the tap-to-attend button, so a native initialize/authenticate/open-meeting sequence can't be triggered twice by an impatient double-tap.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Making Zoom Optional, Per Client
&lt;/h2&gt;

&lt;p&gt;This platform serves multiple clients, and not all of them need video conferencing — some don't offer live classes at all. Bundling a ~280 MB native SDK into every client's build regardless of whether it's ever used isn't a reasonable default, so Zoom needed to be a dependency that could be included or excluded per client rather than something every build carries permanently.&lt;/p&gt;

&lt;p&gt;The first version of this was more literal than it needed to be: dynamically rewriting &lt;code&gt;pubspec.yaml&lt;/code&gt; at build time based on a &lt;code&gt;zoom_enabled&lt;/code&gt; flag, then running &lt;code&gt;flutter pub get&lt;/code&gt; before building each client. It worked, but it made the Dart dependency graph itself something the build had to mutate on every run, which is fragile in ways that are hard to see until CI catches it.&lt;/p&gt;

&lt;p&gt;The version that stuck moved the decision to the build composition layer instead: whether a given client's build includes &lt;code&gt;packages/zoom&lt;/code&gt; — and therefore registers &lt;code&gt;ZoomMeetingService&lt;/code&gt; against &lt;code&gt;MeetingService&lt;/code&gt; — is a per-client build-time decision, resolved once at the composition root rather than by rewriting the dependency manifest at runtime. Clients that don't need conferencing simply don't pull the package in, don't carry the native binaries, and never touch the SQLite/Gradle complexity described above. Clients that do need it get the full &lt;code&gt;MeetingService → ZoomMeetingService&lt;/code&gt; wiring.&lt;/p&gt;

&lt;p&gt;The requirement was never that the Dart abstraction disappear for clients without conferencing — &lt;code&gt;MeetingService&lt;/code&gt; can stay defined in &lt;code&gt;core&lt;/code&gt; either way. The actual requirement was narrower: clients without conferencing shouldn't carry the native Zoom SDK. Once that was the target, the composition-layer approach was the simpler way to hit it. If &lt;code&gt;MeetingService&lt;/code&gt; isn't registered for a given client, that capability just isn't available, and the surrounding UI treats it like any other feature that client doesn't have.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. What the Final Package Actually Looked Like
&lt;/h2&gt;

&lt;p&gt;Zoom's pieces were scattered across a lot of this article, so here's the whole thing in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
└── zoom/
    ├── lib/
    │   └── zoom_meeting_service.dart
    ├── android/
    │   ├── src/main/kotlin/com/testpress/flutter_zoom_meeting_sdk/
    │   ├── libs/
    │   │   └── mobilertc.aar
    │   └── build.gradle.kts
    └── ios/
        ├── MobileRTC.xcframework/
        ├── MobileRTCResources.bundle
        └── zoom.podspec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dart layer&lt;/strong&gt; — &lt;code&gt;ZoomMeetingService&lt;/code&gt;, the concrete implementation of &lt;code&gt;MeetingService&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt; — the Kotlin bridge plus &lt;code&gt;mobilertc.aar&lt;/code&gt;, resolved as a &lt;code&gt;flatDir&lt;/code&gt; dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt; — the Swift bridge plus &lt;code&gt;MobileRTC.xcframework&lt;/code&gt; and its resource bundle, wired through &lt;code&gt;zoom.podspec&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git LFS&lt;/strong&gt; — tracks the native binaries in both platform folders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;core&lt;/code&gt;&lt;/strong&gt; — owns the &lt;code&gt;MeetingService&lt;/code&gt; abstraction and registry; never imports this package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app&lt;/code&gt;&lt;/strong&gt; — the only place that imports &lt;code&gt;zoom&lt;/code&gt; directly, and only for clients that need it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything Zoom-specific stays inside this one package. Nothing outside it needs to know Zoom is what's behind &lt;code&gt;MeetingService&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solutions That Didn't Work
&lt;/h2&gt;

&lt;p&gt;Some of the most useful information here is what I tried and abandoned:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attempt&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Use the upstream plugin directly&lt;/td&gt;
&lt;td&gt;Too much external/native coupling&lt;/td&gt;
&lt;td&gt;SDK and build assumptions baked into the plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit the native binaries normally&lt;/td&gt;
&lt;td&gt;GitHub rejected the push&lt;/td&gt;
&lt;td&gt;Past the 100 MB per-file limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compress the AAR before committing&lt;/td&gt;
&lt;td&gt;Didn't help&lt;/td&gt;
&lt;td&gt;An AAR is already a ZIP archive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pickFirst&lt;/code&gt; on the duplicate &lt;code&gt;libsqlite3.so&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Build succeeded, app crashed at runtime&lt;/td&gt;
&lt;td&gt;ABI/symbol mismatch between the two binaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove &lt;code&gt;sqlite3_flutter_libs&lt;/code&gt; entirely&lt;/td&gt;
&lt;td&gt;Fixed the duplicate, broke Drift elsewhere&lt;/td&gt;
&lt;td&gt;Drift still needs a native SQLite on builds without Zoom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Let &lt;code&gt;courses&lt;/code&gt; call Zoom directly&lt;/td&gt;
&lt;td&gt;Worked, technically&lt;/td&gt;
&lt;td&gt;Wrong dependency direction — domain package coupled to a vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamically rewrite &lt;code&gt;pubspec.yaml&lt;/code&gt; per client&lt;/td&gt;
&lt;td&gt;Worked, but fragile&lt;/td&gt;
&lt;td&gt;Better handled once, at the build composition layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  9. What the Native Dependency Graph Taught Me
&lt;/h2&gt;

&lt;p&gt;At the Dart level, my dependency graph looked harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;courses → core → drift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual build graph was closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;core → drift → sqlite3 → Native Assets → libsqlite3.so
                                              │
zoom → mobilertc.aar → libZMDB.so ───────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A package can introduce native behavior transitively, in ways that are invisible from &lt;code&gt;pubspec.yaml&lt;/code&gt; alone. I wasn't thinking "I added a native SQLite library" — but that's exactly what the dependency graph was doing on my behalf.&lt;/p&gt;

&lt;p&gt;That's also why &lt;code&gt;pickFirst&lt;/code&gt; deserves more suspicion than it usually gets. It's the right tool when two dependencies genuinely ship the same binary. It's the wrong tool when they ship &lt;em&gt;different builds of the same-named library&lt;/em&gt; — at which point it silently converts a build-time error into a runtime linker failure, which is strictly harder to debug. My build-time error (&lt;code&gt;Duplicate libsqlite3.so&lt;/code&gt;) was actually more useful than the alternative would have been, because it forced me to find the real source instead of papering over it.&lt;/p&gt;

&lt;p&gt;The debugging pattern that worked, in order: read the exact build error before touching Gradle; ask why the duplicate exists in the first place; trace the transitive dependency; understand what changed between versions; and fix the source of the conflict rather than the symptom.&lt;/p&gt;




&lt;h2&gt;
  
  
  Principles Worth Keeping
&lt;/h2&gt;

&lt;p&gt;A few things generalize beyond Zoom:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depend on capabilities, not vendors.&lt;/strong&gt; &lt;code&gt;courses → MeetingService&lt;/code&gt;, not &lt;code&gt;courses → Zoom&lt;/code&gt;. The vendor sits behind an interface the domain layer never has to know about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solve dependency conflicts at their source.&lt;/strong&gt; A version constraint that prevents an unwanted binary from being generated is more maintainable than a packaging override that arbitrarily picks between two binaries that already exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A green build isn't proof of a correct native integration.&lt;/strong&gt; &lt;code&gt;pickFirst&lt;/code&gt; can make Gradle stop complaining while the dynamic linker still fails at runtime. Native compatibility has to be validated at runtime, not just at build time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large vendor binaries need their own storage strategy.&lt;/strong&gt; Once your repository contains hundreds of megabytes of native SDK, Git LFS isn't an afterthought — it's part of the architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Own the fork when the third-party wrapper doesn't fit.&lt;/strong&gt; Forking a plugin isn't inherently bad. It's the right call when the native SDK is central to your app, the existing wrapper carries incompatible assumptions, and you need real control over namespaces, build configuration, and reproducibility. The important thing is to own the fork intentionally, rather than accidentally maintaining a fragile copy of someone else's package.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What started as "add Zoom meetings to my Flutter app" turned into a broader exercise in figuring out where responsibility actually belongs: the native SDK behind a plugin boundary, the Zoom-specific implementation in its own package, the &lt;code&gt;courses&lt;/code&gt; domain depending on a capability instead of a vendor, large binaries in a system built for large binaries, and the decision of &lt;em&gt;which clients even carry Zoom&lt;/em&gt; pushed out to the composition root instead of baked into the domain code.&lt;/p&gt;

&lt;p&gt;The mysterious &lt;code&gt;libsqlite3.so&lt;/code&gt; collision turned out to be the most valuable failure in the whole project, because chasing it down — instead of silencing it with &lt;code&gt;pickFirst&lt;/code&gt; — is what taught me the question worth asking whenever two native dependencies collide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When two native dependencies collide, don't immediately ask which one should win. Ask why both are there in the first place.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>mobile</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why Not Every AI Application Needs Vector Embeddings</title>
      <dc:creator>Syed Ibrahim</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:43:40 +0000</pubDate>
      <link>https://dev.to/syed11/why-not-every-ai-application-needs-vector-embeddings-417</link>
      <guid>https://dev.to/syed11/why-not-every-ai-application-needs-vector-embeddings-417</guid>
      <description>&lt;p&gt;&lt;em&gt;How building an AI chapter generator taught me to stop reaching for a vector database first.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I started building my AI chapter generator, I spent hours researching vector databases. I was convinced I needed one.&lt;/p&gt;

&lt;p&gt;I didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;The project was simple on paper. A user uploads a long lecture video, the app transcribes it, figures out where the instructor moves from one topic to the next, and spits out timestamped chapters.&lt;/p&gt;

&lt;p&gt;Before I'd written a single line of the actual feature, my brain had already decided what the architecture should look like. Long text plus AI app equals: chunk it, embed it, store it, retrieve it. That's just the shape everyone's trained to see these days.&lt;/p&gt;

&lt;p&gt;So that's the road I went down. I spent an evening comparing Pinecone, Chroma and FAISS. I read through chunking strategies, fixed size chunks, overlapping windows, semantic chunking, all of it. I had a RAG pipeline half sketched out in my notes app.&lt;/p&gt;

&lt;p&gt;Then I stopped and asked myself something a lot more basic:&lt;/p&gt;

&lt;p&gt;What problem am I actually trying to solve?&lt;/p&gt;

&lt;p&gt;I didn't have a good answer. So I backed up and started over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What embeddings actually do
&lt;/h2&gt;

&lt;p&gt;If you're newer to this stuff, here's the plain version.&lt;/p&gt;

&lt;p&gt;An embedding turns a piece of text into a list of numbers that captures what it means. Text with similar meaning ends up with similar numbers. That's really the whole trick behind it. It turns "how similar are these two ideas" into "how close are these two points," which is something a computer can check almost instantly, even across millions of documents.&lt;/p&gt;

&lt;p&gt;That's a genuinely useful trick, but notice what it's actually for. It's for finding things. If you've got 10,000 support articles and someone asks a question, you can't just feed all 10,000 into the model every time. It wouldn't fit, and even if it somehow did, it'd be painfully slow. Embeddings let you narrow that pile down to the handful of articles that are actually relevant, and only those get handed to the model.&lt;/p&gt;

&lt;p&gt;That narrowing down is retrieval. And retrieval turns out to be a different job than reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval vs reasoning
&lt;/h2&gt;

&lt;p&gt;This is the part that took me a minute to actually get, and I think it's the bit most people gloss over.&lt;/p&gt;

&lt;p&gt;Embeddings solve retrieval. They're good at finding a needle in a haystack you can't fully read.&lt;/p&gt;

&lt;p&gt;LLMs handle reasoning. They think through whatever's in front of them.&lt;/p&gt;

&lt;p&gt;RAG is called Retrieval Augmented Generation because retrieval is just a preprocessing step that decides what the model gets to look at. It doesn't make the model reason any better once it's looking at the right thing. It just makes sure it's looking at the right thing in the first place.&lt;/p&gt;

&lt;p&gt;Which leads to an obvious question. What if the model already has the right thing?&lt;/p&gt;

&lt;p&gt;That was basically my situation. The whole transcript was already right there. Nothing was buried somewhere I needed to dig it out of. There was nothing to find. Just something to work through, in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  But don't long transcripts still need chunking?
&lt;/h2&gt;

&lt;p&gt;Yes, and this tripped me up for a bit, so it's worth spelling out.&lt;/p&gt;

&lt;p&gt;A one hour lecture transcript is way too long to comfortably send in one model call. So you still have to split it up somehow. That part's unavoidable.&lt;/p&gt;

&lt;p&gt;But there are two different reasons you'd chunk something, and I'd been mushing them together in my head without realizing it.&lt;/p&gt;

&lt;p&gt;Chunking for retrieval splits text so you can search it. Each chunk gets embedded and scored against a query, and only the best matches get used.&lt;/p&gt;

&lt;p&gt;Chunking for context just splits text because of size limits. Every chunk still gets used, in order, nothing gets filtered out.&lt;/p&gt;

&lt;p&gt;I needed the second kind. Not "which piece is most relevant," but "here's the whole thing in pieces, go through all of it, in order." That doesn't need embeddings. It just needs a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it clicked
&lt;/h2&gt;

&lt;p&gt;Once I'd separated retrieval from reasoning in my head, and chunking to search from chunking to fit, things got a lot clearer fast.&lt;/p&gt;

&lt;p&gt;I asked myself how a person would do this task. Not an AI, an actual human sitting there watching the lecture.&lt;/p&gt;

&lt;p&gt;They wouldn't be scanning through looking for the paragraph most "similar" to some query, because there's no query. Nobody's asking a question. They'd just watch it from start to finish and notice, naturally, when the instructor wrapped up one idea and moved on to the next.&lt;/p&gt;

&lt;p&gt;That's when it actually clicked. Chapter generation is about sequence, not similarity.&lt;/p&gt;

&lt;p&gt;Embeddings answer "where in this pile does something like X show up." My problem was "where does this one, ordered thing change." Those aren't the same problem wearing different clothes. They're genuinely different problems, and no clever chunking-for-retrieval trick was going to fix that, because retrieval was never actually the job.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I thought I needed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transcript
   │
   ▼
Chunk for retrieval
   │
   ▼
Embed each chunk ──► Vector database
   │                         │
   │      (similarity search)│
   ▼                         ▼
        Query-relevant chunks
                │
                ▼
              LLM
                │
                ▼
            Chapters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I actually built
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transcript
   │
   ▼
Chunk sequentially (for size, not search)
   │
   ▼
LLM reads each chunk in order,
carrying forward a rolling summary
   │
   ▼
Flag topic transitions
   │
   ▼
Merge + align to natural pauses
   │
   ▼
Chapters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same input, same output, half the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually built
&lt;/h2&gt;

&lt;p&gt;Here's the workflow, roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Transcribe the video.&lt;/li&gt;
&lt;li&gt;Split the transcript into sequential chunks of a few minutes each, keeping order and timestamps intact.&lt;/li&gt;
&lt;li&gt;Feed each chunk to the model along with a short rolling summary of everything before it, and ask it to flag where the topic shifts.&lt;/li&gt;
&lt;li&gt;Merge the transitions that land close together, and snap each boundary to the nearest natural pause so a chapter doesn't start mid sentence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is really the whole trick. Each chunk carries context forward from the ones before it. The model isn't staring at six disconnected fragments trying to guess how they relate, it's walking through the lecture the way a person would, carrying the thread as it goes. That continuity is what actually lets it notice "okay, this is where the topic changed." It's also exactly what a similarity search would throw away, since similarity search treats every chunk as its own isolated thing to be scored.&lt;/p&gt;

&lt;p&gt;No vector database, no embedding calls, no retrieval step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real reason I dropped embeddings
&lt;/h2&gt;

&lt;p&gt;I could frame this as a story about cost or speed. Fewer moving parts, lower latency, no vector DB bill. Those were real benefits and I did get them. But that's not actually why I made the change, and leading with that would kind of miss the point.&lt;/p&gt;

&lt;p&gt;The real reason is simpler. Embeddings would have solved a problem I didn't have. My bottleneck was never "how do I find the relevant part of this transcript," because the whole transcript was the relevant part. Bolting retrieval on top wouldn't have made the output better, it would've just added machinery answering a question nobody was asking, while the actual question, where does the topic change, stayed exactly as unsolved as before.&lt;/p&gt;

&lt;p&gt;Cost and latency got better as a side effect of cutting something unnecessary. They weren't the reason it was unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embeddings are still great, when you actually need retrieval
&lt;/h2&gt;

&lt;p&gt;None of this is an argument against embeddings. I still use them all the time, just for a different kind of problem.&lt;/p&gt;

&lt;p&gt;Say you're building a support bot on top of 10,000 help articles. Someone asks a question and there's no way to hand the model all 10,000 articles at once, it needs to find the handful that are actually relevant first. That's exactly the retrieval problem embeddings were built for, and it's a great use of a vector database there.&lt;/p&gt;

&lt;p&gt;The difference between that and my chapter generator isn't the amount of text involved, both had plenty. The difference is whether the model needs to search across a bunch of separate things, or read through one thing it already has in full.&lt;/p&gt;

&lt;p&gt;Needs retrieval: company docs, big knowledge bases, research paper libraries, product manuals, support article collections, long chat history archives.&lt;/p&gt;

&lt;p&gt;Doesn't need retrieval: summarizing a single document, translating text, classifying content, pulling structured data out of one file, reviewing a single codebase, generating chapters from one transcript.&lt;/p&gt;

&lt;p&gt;If the model already has everything it needs sitting in front of it, embeddings don't help it reason better. There's nothing to find, so there's nothing for retrieval to speed up.&lt;/p&gt;

&lt;h2&gt;
  
  
  A question worth stealing
&lt;/h2&gt;

&lt;p&gt;Before I add a vector database to anything now, I ask myself one thing, and it's worth stealing if you don't already ask it:&lt;/p&gt;

&lt;p&gt;Does my AI need to find information, or does it already have it?&lt;/p&gt;

&lt;p&gt;If it already has everything, embeddings won't make the answer any sharper. They'll just add cost, latency, and one more system that can quietly break on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I almost built a whole retrieval pipeline for a problem that had nothing to retrieve, just because that's the default move everyone reaches for. The real lesson here wasn't about embeddings specifically. It was about catching myself solving the problem the tutorials describe instead of the one actually sitting in front of me.&lt;/p&gt;

&lt;p&gt;The best architecture isn't the one with the most pieces. It's the one that solves the actual problem with the least stuff bolted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it in action
&lt;/h2&gt;

&lt;p&gt;The ideas in this article weren't just theoretical—they came from building a real application.&lt;/p&gt;

&lt;p&gt;If you'd like to try it yourself, here's the live demo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://trywaypoint.streamlit.app/" rel="noopener noreferrer"&gt;https://trywaypoint.streamlit.app/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>database</category>
      <category>rag</category>
    </item>
    <item>
      <title>OpenSpec: The Missing Layer Between You and Your AI Coding Assistant</title>
      <dc:creator>Syed Ibrahim</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:14:35 +0000</pubDate>
      <link>https://dev.to/syed11/openspec-the-missing-layer-between-you-and-your-ai-coding-assistant-2a71</link>
      <guid>https://dev.to/syed11/openspec-the-missing-layer-between-you-and-your-ai-coding-assistant-2a71</guid>
      <description>&lt;p&gt;If you have spent any time doing AI-assisted development or vibe coding, you already know the problem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You open a new chat.&lt;/li&gt;
&lt;li&gt;Start building something.&lt;/li&gt;
&lt;li&gt;The AI just runs with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No alignment, no scope, no shared understanding of what you are actually trying to do.&lt;/p&gt;

&lt;p&gt;That works fine for small throwaway tasks. But when you are working on a real codebase, things go sideways fast.&lt;/p&gt;

&lt;p&gt;The AI makes assumptions. Scope creeps. You end up with code that technically works but does not match what you had in mind.&lt;/p&gt;

&lt;p&gt;OpenSpec fixes this by introducing one simple idea&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;agree on what you are building before any code gets written.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It gives you and your AI coding assistant a lightweight structure to follow for every change you make. Before implementation starts, you define the intent, the scope, and the approach. The AI reads that and works within those boundaries instead of guessing.&lt;/p&gt;

&lt;p&gt;The result is more predictable output, less back and forth, and something that matters a lot in fast-paced development ~ the reasoning behind your decisions is actually preserved instead of disappearing into chat history.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is OpenSpec?
&lt;/h2&gt;

&lt;p&gt;OpenSpec is a spec-driven development framework. It sits between you and your AI coding assistant as a lightweight planning layer.&lt;/p&gt;

&lt;p&gt;The idea is simple. Before your AI writes a single line of code, you and the AI agree on a spec. What you are building, why you are building it, how you are going to build it, and what is out of scope. Everything is written down and locked in before implementation starts.&lt;/p&gt;

&lt;p&gt;It works with the tools you already use ~ Claude Code, Cursor, Codex, Copilot, Antigravity and more. No API keys, no complex setup. Your specs live right inside your repository as markdown files.&lt;/p&gt;

&lt;p&gt;Every change you make in OpenSpec goes through four artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proposal&lt;/strong&gt; ~ the why. What you want to build and why it matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specs&lt;/strong&gt; ~ the what. The requirements and expected behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design&lt;/strong&gt; ~ the how. The technical approach and decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks&lt;/strong&gt; ~ the steps. Broken down implementation plan&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Installation and Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Node.js 20.19.0 or higher&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @fission-ai/openspec@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initialize
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;your-project&amp;gt;
openspec init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3jlix3tip4lhrkkluu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3jlix3tip4lhrkkluu3.png" alt="Terminal screenshot of openspec init welcome screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once initialized, it will ask you to select which AI tools you want to set up. OpenSpec supports 28 tools including Claude Code, Cursor, Codex, Copilot and more. Use Space to toggle and Enter to confirm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbl2ezmxf6lhb02okkpz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbl2ezmxf6lhb02okkpz6.png" alt="Terminal screenshot of openspec tool selection"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After selecting your tools, OpenSpec sets everything up automatically ~ agent skills, slash commands, and config files for each tool you selected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbecmswgh7yrs78p912p6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbecmswgh7yrs78p912p6.png" alt="Terminal screenshot of openspec setup complete"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once setup is complete, restart your IDE for the slash commands to take effect. Then you are ready to start your first change with &lt;code&gt;/opsx:propose &amp;lt;what-you-want-to-build&amp;gt;&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four Artifacts
&lt;/h2&gt;

&lt;p&gt;Every change in OpenSpec progresses through four artifacts.&lt;/p&gt;

&lt;p&gt;Each artifact builds on the previous one, ensuring implementation starts only after the change is fully understood.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Proposal
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Why&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proposal captures the intent behind the change.&lt;/p&gt;

&lt;p&gt;Questions answered here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why are we doing this?&lt;/li&gt;
&lt;li&gt;What problem are we solving?&lt;/li&gt;
&lt;li&gt;What value does this provide?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where intent gets locked in.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Specs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The What&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Specs define the requirements and expected behavior.&lt;/p&gt;

&lt;p&gt;Questions answered here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should happen?&lt;/li&gt;
&lt;li&gt;What should the system do?&lt;/li&gt;
&lt;li&gt;How should it behave?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These become the source of truth during implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Design
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The How&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Design documents describe the technical approach.&lt;/p&gt;

&lt;p&gt;Questions answered here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which architecture should we use?&lt;/li&gt;
&lt;li&gt;What tradeoffs are involved?&lt;/li&gt;
&lt;li&gt;What technical decisions need to be made?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase often prevents expensive mistakes before they reach production.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Tasks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tasks break the implementation into manageable pieces.&lt;/p&gt;

&lt;p&gt;Questions answered here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What needs to be done?&lt;/li&gt;
&lt;li&gt;In what order?&lt;/li&gt;
&lt;li&gt;How do we know we're finished?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This becomes the execution plan for both humans and AI agents.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your First Change
&lt;/h2&gt;

&lt;p&gt;Let's walk through a real example. We are going to add a new User Profile API endpoint using OpenSpec from start to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start the Change
&lt;/h3&gt;

&lt;p&gt;Run the propose command in your AI coding assistant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opsx:propose "add-user-profile-api-endpoint"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your AI will generate all four artifact files automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Created openspec/changes/add-user-profile-api-endpoint/
✓ proposal.md — why we're doing this, what's changing
✓ specs/       — requirements and scenarios
✓ design.md    — technical approach
✓ tasks.md     — implementation checklist
Ready for implementation!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what each artifact looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;proposal.md&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Proposal: Add User Profile API Endpoint&lt;/span&gt;

&lt;span class="gu"&gt;## Intent&lt;/span&gt;
Users need a way to retrieve and update their profile information
through a dedicated API endpoint.

&lt;span class="gu"&gt;## Scope&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; GET /api/users/:id endpoint
&lt;span class="p"&gt;-&lt;/span&gt; PUT /api/users/:id endpoint
&lt;span class="p"&gt;-&lt;/span&gt; Input validation and error handling

&lt;span class="gu"&gt;## Approach&lt;/span&gt;
Use existing REST conventions with JWT authentication middleware.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;specs/api/spec.md&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Delta for API&lt;/span&gt;

&lt;span class="gu"&gt;## ADDED Requirements&lt;/span&gt;

&lt;span class="gu"&gt;### Requirement: User Profile Retrieval&lt;/span&gt;
The system MUST expose a GET endpoint to retrieve user profile data.

&lt;span class="gu"&gt;#### Scenario: Fetch profile&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; GIVEN an authenticated user
&lt;span class="p"&gt;-&lt;/span&gt; WHEN GET /api/users/:id is called
&lt;span class="p"&gt;-&lt;/span&gt; THEN the user profile is returned
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;design.md&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Design: Add User Profile API Endpoint&lt;/span&gt;

&lt;span class="gu"&gt;## Approach&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use Express router for route handling
&lt;span class="p"&gt;-&lt;/span&gt; JWT middleware for authentication
&lt;span class="p"&gt;-&lt;/span&gt; Joi for input validation
&lt;span class="p"&gt;-&lt;/span&gt; Return 404 if user not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;tasks.md&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Tasks&lt;/span&gt;

&lt;span class="gu"&gt;## 1. Route Setup&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 1.1 Create user profile route file
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 1.2 Register route in main router

&lt;span class="gu"&gt;## 2. Handlers&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 2.1 Implement GET /api/users/:id handler
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 2.2 Implement PUT /api/users/:id handler

&lt;span class="gu"&gt;## 3. Validation and Error Handling&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 3.1 Add input validation
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 3.2 Add error handling middleware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 2: Implement
&lt;/h3&gt;

&lt;p&gt;Once you are happy with all the artifacts, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opsx:apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your AI now works through the tasks one by one following the spec exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Working through tasks...
✓ 1.1 Created user profile route file
✓ 1.2 Registered route in main router
✓ 2.1 Implemented GET /api/users/:id handler
✓ 2.2 Implemented PUT /api/users/:id handler
✓ 3.1 Added input validation
✓ 3.2 Added error handling middleware
All tasks complete!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No guessing, no assumptions, no scope creep. Your AI follows the spec and nothing else.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Archive
&lt;/h3&gt;

&lt;p&gt;Once everything is verified, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opsx:archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This closes the change completely. It merges your delta specs into the main specs folder and moves the change to the archive permanently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Archiving add-user-profile-api-endpoint...
✓ Merged specs into openspec/specs/api/spec.md
✓ Moved to openspec/changes/archive/2025-01-24-add-user-profile-api-endpoint/
Done! Ready for the next feature.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasoning behind every decision is now preserved in your repository forever. Next time anyone touches this code ~ including an AI agent ~ the why is already there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tips from Real Usage
&lt;/h2&gt;

&lt;p&gt;After using OpenSpec for a few months in fast paced AI driven development, here are a few things I have learned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always review the proposal before moving on&lt;/strong&gt;&lt;br&gt;
The proposal is where intent gets locked in. If something is off there, it carries through to every other artifact. Spend an extra minute here and save yourself a lot of back and forth later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be specific with your scope&lt;/strong&gt;&lt;br&gt;
The more specific you are about what is out of scope, the better your AI performs. Writing "out of scope: custom error pages" is more useful than leaving it vague. AI agents take boundaries seriously when they are written down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not skip the design artifact&lt;/strong&gt;&lt;br&gt;
It is tempting to jump straight to implementation. But the design phase is where you catch bad technical decisions before they become bad code. Many times the design phase alone has saved me from going down the wrong path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it on existing codebases&lt;/strong&gt;&lt;br&gt;
OpenSpec is brownfield first. You do not need a fresh project to get value from it. Drop it into an existing codebase and start using it for your next change. The value shows up immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your specs are living documentation&lt;/strong&gt;&lt;br&gt;
Every time you archive a change, your specs folder gets more complete. Over time it becomes a genuine source of truth for how your system works and why decisions were made. That is valuable for your whole team, not just for AI agents.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;AI coding assistants are powerful. But power without direction is just noise.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;OpenSpec is not about slowing down. It is about making sure that when you move fast, you are moving in the right direction. In vibe coding and fast paced AI driven development, the biggest risk is not writing bad code. It is building the wrong thing confidently.&lt;/p&gt;

&lt;p&gt;After using OpenSpec for a few months, the biggest shift I noticed was not in the code quality. It was in how much context was being preserved. The why behind decisions no longer lives in chat history or someone's head. It lives in the repository, right next to the code it describes.&lt;/p&gt;

&lt;p&gt;If you are doing any kind of AI assisted development and you are not using a spec driven approach, give OpenSpec a try. It takes five minutes to set up and the value shows up on your very first change.&lt;/p&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opsx:propose "your-first-change"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And see what happens.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>aiassisteddevelopment</category>
      <category>developertools</category>
      <category>ai</category>
    </item>
    <item>
      <title>Google Just Made AI Memory Official at Cloud NEXT '26 - And It Made Me Uncomfortable :(</title>
      <dc:creator>Syed Ibrahim</dc:creator>
      <pubDate>Sun, 26 Apr 2026 11:39:11 +0000</pubDate>
      <link>https://dev.to/syed11/google-just-made-ai-memory-official-at-cloud-next-26-and-it-made-me-uncomfortable--452d</link>
      <guid>https://dev.to/syed11/google-just-made-ai-memory-official-at-cloud-next-26-and-it-made-me-uncomfortable--452d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'll be honest. I didn't sit through the full Google Cloud NEXT '26 keynotes. I caught the highlights in between work. That's my level of "following the news."&lt;/p&gt;

&lt;p&gt;But one announcement stopped me: &lt;strong&gt;Agent Memory Bank&lt;/strong&gt;, now generally available inside the Gemini Enterprise Agent Platform.&lt;/p&gt;

&lt;p&gt;Here's what it does in plain English: your AI agent can now remember things across conversations. Not just inside one session, but across sessions. It dynamically builds memory from past interactions, stores it, and pulls it back when relevant. Using Memory Profiles, agents can recall high-accuracy details with low latency so they don't lose context. (&lt;a href="https://cloud.google.com/blog/topics/google-cloud-next/google-cloud-next-2026-wrap-up" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Sounds great on paper. But it made me think about something that happened to me recently.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Already Experienced AI Memory. Without Asking for It.
&lt;/h2&gt;

&lt;p&gt;A few weeks ago I was trying to fix a Decoder init failure on specific Snapdragon/MediaTek chipset phones. It was showing up in Sentry on our Android player SDK, ExoPlayer related. So I was using an AI tool to work through it, asking questions, going back and forth.&lt;/p&gt;

&lt;p&gt;At some point I closed that chat and started a completely new one. Different day, different question. But somewhere in that new conversation the AI referenced something specific from the ExoPlayer session. Something I hadn't brought up at all.&lt;/p&gt;

&lt;p&gt;So I asked: "are you reading my previous chats?"&lt;/p&gt;

&lt;p&gt;It said no.&lt;/p&gt;

&lt;p&gt;I gave it the exact context it had referenced. Still denied it.&lt;/p&gt;

&lt;p&gt;That's the part that didn't sit right with me. Not that it remembered. That it remembered and then said it didn't. If a system can recall something it claims it cannot access, and then confidently insists otherwise, that's not just a bug. That's a trust problem. Because now I'm sitting there wondering what else it knows that it's not telling me. And the easy fallback is always "AI can make mistakes" which okay, sure, but that line is doing a lot of heavy lifting. It quietly removes accountability. The system moves on. I'm left holding the confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now Google Is Making Memory an Official Feature
&lt;/h2&gt;

&lt;p&gt;Agent Engine Sessions and Memory Bank, which give agents persistent context across interactions, are now generally available. (&lt;a href="https://thenextweb.com/news/google-cloud-next-ai-agents-agentic-era" rel="noopener noreferrer"&gt;The Next Web&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;In some ways that's actually more honest. At least now it's documented, intentional, and in an API you can actually inspect.&lt;/p&gt;

&lt;p&gt;The idea makes sense for what I build. I work on Django-based AI SaaS. Right now every session resets. Users repeat themselves. Agents forget context. Google showed a demo where a customer moves from text chat to a phone call and the agent seamlessly remembers exactly where they left off. (&lt;a href="https://cloud.google.com/blog/topics/google-cloud-next/next26-day-1-recap" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt;) That kind of continuity is genuinely useful.&lt;/p&gt;

&lt;p&gt;Think of it like a &lt;code&gt;user_preferences&lt;/code&gt; table in Django, except Google manages the entire memory layer for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  But My Trust Questions Remain
&lt;/h2&gt;

&lt;p&gt;If unofficial memory already existed and got denied, what happens with the official version?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What exactly gets stored, and for how long?&lt;/li&gt;
&lt;li&gt;Can my users actually delete their memory and trust it's gone?&lt;/li&gt;
&lt;li&gt;How do I explain this to users in India who are already cautious about data privacy?&lt;/li&gt;
&lt;li&gt;And most practically, where is the clean &lt;code&gt;pip install&lt;/code&gt; Django walkthrough?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The codelab demo showed Memory Bank costing less than $5 (&lt;a href="https://codelabs.developers.google.com/next26/dev-keynote/enhancing-agents-with-memory?hl=en" rel="noopener noreferrer"&gt;Google Codelabs&lt;/a&gt;), which is a good sign. But I need to see full production pricing before I build anything serious on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Prediction
&lt;/h2&gt;

&lt;p&gt;Persistent agent memory is the right direction. Google's pitch is a unified stack: chips designed for models, models grounded in your data, agents built with those models, all secured by the infrastructure. (&lt;a href="https://cloud.google.com/blog/topics/google-cloud-next/welcome-to-google-cloud-next26" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt;) If that holds up, it could genuinely be the default for AI SaaS in 12 months.&lt;/p&gt;

&lt;p&gt;But the trust layer has to come with the feature. Users need to know what's remembered, what's stored, and what "delete" actually means.&lt;/p&gt;

&lt;p&gt;Google has a real chance to set that standard properly with Agent Memory Bank, especially because the unofficial version already exists and nobody is really talking about it clearly.&lt;/p&gt;

&lt;p&gt;Make it transparent, Google. The feature is good. The trust needs to catch up.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
      <category>vertexai</category>
    </item>
  </channel>
</rss>
