Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - tawk/CoreDataBrowser: Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network · GitHub
Skip to content

Repository files navigation

CoreDataBrowser

A modern, drop-in Core Data inspector for iOS / iPadOS / macOS / tvOS / Mac Catalyst apps. Boots a tiny HTTP server inside your app and exposes the contents of any NSManagedObjectContext to a desktop browser on the same local network. Inspired by the original iOS-Hierarchy-Viewer, scoped to Core Data browsing only.

Screenshot of CoreDataBrowser's web UI: sidebar entity list, records grid, and right-side detail inspector with attributes, relationships, and content viewer.

The bundled web UI: sidebar entity list with search, multi-tab open entities at the top, a center records grid with sortable / resizable / reorderable / hideable columns, and a right-side detail inspector that stacks attributes, relationships, and a content viewer. Pane sizes, column preferences, and open tabs persist in localStorage.

  • Read-only by default; opt in to edit + delete with Options.readOnly = false.
  • Manual refresh — the web UI re-queries on demand.
  • Zero UI changes in your app — open the URL printed at launch in any browser.
  • Pure Swift Package, one dependency (Swifter).

Features

Highlights:

  • Sidebar entity list with row counts.
  • Records grid with sortable / resizable / reorderable / hideable columns; preferences persisted in localStorage per entity.
  • Per-record detail inspector — every attribute, plus a relationships summary with clickable items (browser back button works).
  • Relationships sub-panel — pick a relationship of the selected row from a dropdown, click an item to navigate.
  • Content viewer — pick an attribute of the selected row to read its full value.
  • Multiple NSManagedObjectContext support — register many in one server; the top bar shows a context switcher when there's more than one.
  • Manual refresh; pane sizes persisted across reloads.
  • Edit mode (opt-in): edit attribute values, delete records, upload binary blobs up to 10 MB. Relationships stay read-only. Disabled by default — writes return 405 until Options.readOnly = false.

Routes: the UI is served at / (and aliased at /lab / /index.html so old links keep working).

Requirements

  • iOS 15+ / iPadOS 15+ / macOS 12+ / tvOS 15+ / Mac Catalyst 15+
  • Swift 5.9+
  • A Core Data NSManagedObjectContext (viewContext or a private-queue context)
  • Local Wi-Fi between the device and the machine running the desktop browser

Installation

Xcode (recommended)

  1. File → Add Package Dependencies…
  2. Paste the package URL, e.g. https://github.com/<you>/CoreDataBrowser.git, or pick Add Local… and select the CoreDataBrowser/ folder.
  3. Add the CoreDataBrowser library to your app target.

Package.swift

dependencies:[.package(url:"https://github.com/<you>/CoreDataBrowser.git", from:"1.0.0")],targets:[.target(name:"MyApp", dependencies:["CoreDataBrowser"])]

Local package

Reference the package from any consuming iOS app project via an absolute or relative path. From Package.swift:

dependencies:[.package(path:"/Users/<you>/iOS/CoreDataBrowser")]

Or in Xcode: File → Add Package Dependencies… → Add Local… and select the CoreDataBrowser folder. Xcode writes the path as an XCLocalSwiftPackageReference in the consuming app's .xcodeproj and rebuilds the package as a sibling target.

Quick start

import SwiftUI
import CoreData
import CoreDataBrowser
@mainstructMyApp:App{letpersistence=PersistenceController.shared
#if DEBUGprivateletbrowser:CoreDataBrowserServer@StateprivatevarbrowserURLs:[URL]=[]init(){
browser =CoreDataBrowserServer(context: persistence.container.viewContext)}#endifvarbody:someScene{WindowGroup{ContentView()#if DEBUG.onAppear {do{letinfo=try browser.start()
browserURLs = info.urls
info.urls.forEach{print("CoreDataBrowser:", $0.absoluteString)}}catch{print("CoreDataBrowser failed to start:", error)}}#endif}}}

Console output on launch:

CoreDataBrowser: http://192.168.1.42:8080
CoreDataBrowser: http://127.0.0.1:8080

Open either URL from a desktop browser on the same network — the LAN address from another machine, 127.0.0.1 from the same machine (which works for the iOS Simulator since it shares the host's network stack).

Configuration

varopts=CoreDataBrowserServer.Options()
opts.port =9000 // base port; +1…+10 retried if busy
opts.bindAddress =.loopback // only reachable from the device itself
letserver=CoreDataBrowserServer(context: ctx, options: opts)
OptionDefaultDescription
port8080Preferred port. If busy, port+1 … port+10 is tried.
bindAddress.allInterfaces.allInterfaces (0.0.0.0) or .loopback (127.0.0.1 only).
readOnlytrueWhen true, PATCH/DELETE return 405. Set to false to enable edit + delete from the web UI (attributes only; relationships stay read-only).

Edit mode

varopts=CoreDataBrowserServer.Options()
opts.readOnly =falseletserver=CoreDataBrowserServer(context: ctx, options: opts)

When readOnly = false:

  • The brand chip in the top-left flips from read-only to read/write.
  • The detail pane gains Edit and Delete buttons. Click Edit to render typed inputs per attribute (textareas for strings, date + time pickers for Date, file picker for Binary, etc.), then Save to PATCH or Cancel to discard.
  • Binary uploads are capped at 10 MB; oversize files are rejected client-side and re-rejected by the server. Transformable and ObjectID attributes are not editable (they're shown with a "not editable" tag).
  • Relationships are not editable from the web UI.
  • Leaving the binary file picker empty preserves the existing blob — only attributes the user actually touched are written.
  • Saves run through validateForUpdate() then context.save() on the context's own queue; validation errors come back as 400 with the underlying message.

start() returns a RunningInfo with the bound port and every URL the server can be reached at. You can surface those in your app's UI — handy on physical devices when the console isn't readable.

Multiple contexts

A single CoreDataBrowserServer can browse several named NSManagedObjectContext instances side-by-side — useful when your app has more than one Core Data stack (e.g. Main + Analytics, or separate stores per user). The web UI shows a context switcher in its top bar; clients tag requests with ?ctx=<name>.

letserver=CoreDataBrowserServer(contexts:[.init(name:"Main", context: persistence.container.viewContext),.init(name:"Analytics", context: analytics.container.viewContext),])try server.start()
  • The first context registered is the default (used when the URL has no ?ctx=).
  • Duplicate names are dropped (first one wins).
  • The single-context init (CoreDataBrowserServer(context:)) still works — it just registers one context named context.name ?? "default".
  • The active context is remembered in localStorage["cdb.context"] across reloads, and is also written into the URL (/?ctx=Main&entity=Book&id=…) so reload/share preserves state.

If you'd rather run separate servers on different ports (e.g. one per build configuration), construct multiple CoreDataBrowserServer instances with different port values — each one finds a free port on start().

iOS Info.plist notes

For the basic case (an inbound TCP listener on a non-privileged port), no Info.plist changes are required:

  • NSAppTransportSecurity exceptions apply to outbound HTTP only — the iOS app receiving HTTP requests doesn't need them.
  • NSLocalNetworkUsageDescription is required for outbound mDNS / .local browsing, not for passive listening.

Recommended to add anyway, to keep things smooth on physical devices and prepare for future Bonjour support:

<key>NSLocalNetworkUsageDescription</key>
<string>Allow this debug build to expose its Core Data store to your local network.</string>

Security

  • Never ship this in production builds. Always wrap in #if DEBUG.
  • The server has no authentication. Anyone on the same Wi-Fi can read the entire store.
  • With readOnly = true (the default), assume any field can be exfiltrated. With readOnly = false, anyone on the LAN can also modify or delete records and upload arbitrary 10 MB blobs into binary attributes — only flip the switch on disposable debug data.
  • For higher safety, set bindAddress = .loopback and forward a port from your Mac to the device/simulator when needed. This is especially worth doing whenever readOnly = false.

HTTP API

MethodPathDescription
GET/The bundled web UI (aliased at /lab and /index.html).
GET/lab.{css,js}Web UI assets.
GET/api/health{ ok, store, readOnly, capabilities }
GET/api/contexts[{ name, store, isDefault }] — all registered contexts.
GET/api/entities?ctx=All entities + counts + schema, for the named (or default) context.
GET/api/entities/:name?ctx=&search=&searchAttr=&sort=&order=asc|desc&limit=&offset=Paginated records.
GET/api/object?ctx=&id=<uri>Full record by NSManagedObjectID URI (URL-encoded).
GET/api/object/export?ctx=&id=<uri>Same payload, served as a JSON download.
PATCH/api/object?ctx=&id=<uri>Body {"attrs": {<name>: <value>, ...}}. Only listed attributes are touched; relationships and Transformable/ObjectID are refused. Binary values are base64 strings (≤ 10 MB). Returns the freshly-saved record. Disabled unless readOnly = false.
DELETE/api/object?ctx=&id=<uri>Returns {"ok": true, "id": "<uri>"}. Disabled unless readOnly = false.

ctx is optional on every data endpoint — omit it to query the default (first-registered) context. An unknown context name returns 404 { "error": "Unknown context: <name>" }. Other errors are JSON { "error": "<message>" } with status codes 400 / 404 / 405 / 500.

Troubleshooting

  • Can't reach the URL from my laptop. Both devices must be on the same network, and the network must not isolate clients (some hotel/guest Wi-Fis do). Use the IP printed for the en0 interface; ignore IPv6.
  • Port already in use. The server retries port + 1 up to port + 10. If all are busy, start() throws — pass a different base port.
  • Refreshing doesn't show my latest changes. The server reads through the context you handed it. If your app writes on a private background context that doesn't merge into viewContext, hand the background context to the server, or fix your merge policy.
  • Simulator: only 127.0.0.1 works. The simulator shares the Mac's network stack — that address works from the host machine. From another machine on the LAN, use the Mac's LAN IP.
  • VPN / iCloud Private Relay. Can route LAN traffic unexpectedly. Disable for testing.

License

MIT — see LICENSE.

About

Browse your iOS app's Core Data store from any web browser. A drop-in Swift Package that boots a tiny HTTP server inside your app and exposes any NSManagedObjectContext over the local network

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages