Engineering · 7 min read

Connecting Spark to Muse Without Moving My Data

How I extended Spark beyond ChatGPT using MCP, OAuth, and a shared backend, without duplicating my ideas or rebuilding the application.

Spark wasn’t supposed to be tied to ChatGPT

When I first built Spark, ChatGPT was the only AI assistant I needed it to work with.

The idea was simple: instead of losing ideas across conversations, I could save them to Spark and return to them whenever I wanted. ChatGPT handled the conversation, while Spark handled the data.

But after getting the initial integration working, I started thinking about something else.

What if I wanted to use those same ideas in another AI assistant?

I was already exploring Muse, and I didn’t want to start over with a separate collection of ideas. If I saved something through ChatGPT, I wanted it to be available when I opened Muse, too.

That raised an interesting engineering question: Could I connect another AI assistant to Spark without building a second version of Spark?

Fortunately, the way I’d designed the original integration gave me a good starting point.

One backend, multiple AI assistants

When I built Spark, I separated the conversational interface from the application responsible for storing and managing ideas.

ChatGPT communicated with Spark through its Model Context Protocol (MCP) server. The server exposed the tools, handled requests, and worked with the underlying data in Supabase.

That separation meant the ideas weren’t actually stored inside ChatGPT.

They belonged to Spark.

So rather than creating a separate Muse integration with its own database, I decided to extend the existing architecture.

Both ChatGPT and Muse would connect to the same MCP server, use the same tools, and work with the same collection of ideas.

The goal was to keep the experience consistent regardless of which AI assistant I was using.

If I saved an idea through ChatGPT, Muse should be able to retrieve it. If I updated that idea through Muse, ChatGPT should see the updated version the next time it requested it.

No synchronization service. No duplicated records. Just one application with multiple ways to interact with it.

ChatGPT

Original approved OAuth client

Muse

Separately registered OAuth client

↓ Separate credentials and client-bound access tokens ↓

One Spark MCP server

Owner and resource validation · Eight shared tools

↓

One Supabase PostgreSQL idea collection

Existing ideas, lifecycle, notes, and history

Logical architecture of the merged implementation. Muse live tool discovery and retrieval remain unverified. The clients do not share OAuth credentials.

The real challenge was authentication

The MCP architecture already supported the idea of multiple clients. The more interesting challenge was deciding how to let another AI assistant connect securely.

Spark originally had an OAuth configuration designed around ChatGPT.

OAuth allows an application to request access to another service without simply sharing a user’s credentials. For Spark, it provides the authorization flow that lets an approved AI client interact with my account.

Adding Muse meant supporting another OAuth client without weakening the rules I’d already established.

I didn’t want to reuse ChatGPT’s client identity for Muse. They are separate applications, and Spark should be able to distinguish which one is requesting access.

I registered Muse as its own OAuth application and extended Spark’s configuration to recognize multiple approved clients.

Each client has its own identity and an explicitly configured callback address, which tells the authorization flow where it is allowed to return after access is approved.

Instead of opening Spark to any application that knew its server address, I kept an allowlist of trusted clients.

That gave me a way to expand the integration deliberately while retaining control over who could connect.

Keeping the security boundaries intact

Supporting another client wasn’t as simple as adding its ID to a configuration file.

I also needed to make sure the existing authentication and authorization checks still applied.

Spark uses access tokens to determine whether a request is authorized. Those tokens need to be valid, intended for Spark, associated with the correct account, and issued through an approved client.

I extended the validation to recognize Muse while preserving the existing checks for token signatures, expiration, issuer, audience, account ownership, and role.

The audience check was particularly important.

Just because someone has successfully signed in doesn’t mean every token they possess should grant access to Spark’s MCP tools. The token needs to be intended for that resource.

I also updated the Supabase token hook, which controls the claims included when access tokens are issued. It now recognizes the approved client identities while retaining Spark’s owner-only restrictions.

For approved OAuth clients, the hook supplies Spark’s resource audience and limits token lifetime to fifteen minutes.

The goal wasn’t to invent a different security model for Muse.

It was to extend the one I’d already built without introducing a shortcut around it.

Making sure the callback belongs to the right client

One detail I had to account for was what happens after someone approves access.

An OAuth flow eventually needs to return control to the application that requested authorization.

With only ChatGPT connected, Spark had one approved client to consider. Adding Muse introduced another possible destination.

I didn’t want Spark to accept a callback just because it appeared somewhere in the approved configuration.

The callback needed to belong to the specific client that started the authorization request.

I updated the consent flow to preserve that client identity and verify the final callback against the corresponding approved address.

The existing protections around request origins, CSRF, secure cookies, and account ownership remained in place.

This is the kind of detail that can be easy to overlook when adding a second integration. The visible goal is to connect another app, but the implementation needs to preserve the trust boundaries between them.

One trade-off I chose to keep

There was also an edge case involving existing OAuth grants.

Under certain conditions, Supabase can return a redirect without providing enough information for Spark to reliably determine which client initiated the request.

With multiple approved clients, I didn’t want to guess.

I chose to reject that ambiguous redirect rather than accept a callback based only on its destination.

The trade-off is that an affected client may need its existing grant revoked before starting a fresh authorization flow.

That’s not the smoothest reconnection experience, but it’s a limitation I preferred to keep rather than loosen the validation.

It’s also a reminder that integration work sometimes involves choosing between convenience and a security boundary. In this case, I prioritized knowing which client Spark was authorizing.

What didn’t need to change

One of my favorite parts of this implementation was how much of Spark I didn’t have to touch.

The same eight MCP tools remained available:

The underlying PostgreSQL database stayed the same. So did the idea lifecycle, history, ownership rules, and row-level security policies.

I didn’t need to introduce a synchronization process between ChatGPT and Muse because both clients were designed to work with the same backend.

The changes were concentrated around authentication and client authorization, which was exactly where I wanted them.

That was a useful validation of the original architecture.

I hadn’t designed Spark around a second AI assistant from day one, but separating the interface from the data made that expansion much more manageable.

Testing the integration

I also wanted to make sure adding Muse didn’t break the existing ChatGPT integration.

The implementation included automated tests for both approved client identities, including whether they could discover the same eight MCP tools and retrieve the same mocked test idea through the shared server logic.

I tested rejection cases as well, including invalid or expired tokens, incorrect audiences, unknown clients, and mismatched authorization callbacks.

Those checks helped validate that extending the approved-client list hadn’t bypassed the original access controls.

The implementation was merged and deployed to Spark’s production environment on October 30, 2026.

There’s an important distinction between that milestone and a fully verified live Muse session. The implementation and automated tests establish the multi-client authorization behavior, but the development record available for this article doesn’t yet confirm a complete production Muse connection, including token refresh and retrieval of an existing idea.

That’s the remaining end-to-end validation I want to complete.

What I learned from extending Spark

Building Spark taught me how to connect an AI assistant to an application I’d created.

Extending it to Muse taught me something different: the value of designing boundaries that allow software to evolve.

Because Spark owned its data and exposed its functionality through MCP, adding another conversational interface didn’t require rebuilding the core application.

The work was largely about establishing trust for another client.

It also reinforced something I try to keep in mind when building software: adding a feature shouldn’t mean weakening the decisions that made the original system reliable.

I could have taken shortcuts to make the OAuth flow more permissive. Instead, I chose to preserve explicit client identities, callback validation, and owner-only authorization, even where that introduced a reconnection limitation.

The result is an architecture that can support more than one approved AI client while keeping a single source of truth for my ideas.

And that’s what I find most interesting about Spark.

The ideas belong to me, not to whichever AI assistant I happen to be using.

Spark gives me a way to build around that principle, one integration at a time.