Amethyst Vein – A cross-platform, open-source SwiftData replacement

miakoring1 pts1 comments

Amethyst Vein - A cross-platform, open source, SwiftData alternative - Community Showcase - Swift Forums

= 40rem)" rel="stylesheet" data-target="discourse-reactions_desktop" /><br>= 40rem)" rel="stylesheet" data-target="poll_desktop" />

Amethyst Vein - A cross-platform, open source, SwiftData alternative

Community Showcase

swift,<br>databases,<br>swiftdata,<br>cross-platform

MiaKoring

(Mia Koring)

August 17, 2026, 12:55am

TLDR : Amethyst Vein is a SwiftData inspired database framework, with UI support for SwiftUI and SwiftCrossUI out of the box, with a UI independent surface available too. It's compatible with Apple, Linux, Android and Windows.

Links: Repo | Docs & Tutorials

The Swift ecosystem is continuously expanding, I love Swift and I love making apps for myself. But I need them on more than just Apple devices, which is not as pleasant as inside the Apple ecosystem yet.

Contributing to SwiftCrossUI accounts for the UI part, but storing data is still a bit annoying. There are ORMs, but to me they feel a bit out of place in a declarative UI framework. And aside from SwiftDatas problems (these absolutely exist) I really like the API of SwiftData and how it integrates with SwiftUI.

With a combination of underestimating the challenge, curiosity and a goal in mind, I started building the solution: Amethyst Vein.

This is an example of how it can look (ignore the content of the example not making too much sense on the clientside, I was lacking ideas):

enum V0_0_1: VersionedSchema {<br>static let version = ModelVersion(0, 0, 1)<br>static let models: [any PersistentModel.Type] = [<br>Post.self,<br>Attachment.self

@Model<br>final class Post {<br>var title: String<br>var content: String

@Relationship(<br>inverse: \Attachment.post,<br>deleteRule: .cascade<br>var attachments: [Attachment]

init(title: String, content: String) {<br>self.title = title<br>self.content = content

@Model<br>final class Attachment {<br>@Relationship<br>var post: Post?

var name: String<br>var fileType: FileType<br>var sizeMiB: Double

@LazyField<br>var data: Data?

init(name: String, fileType: FileType, data: Data) {<br>self.name = name<br>self.fileType = fileType<br>self.sizeMiB = Double(data.count) / 1024 / 1024<br>self.data = data

enum FileType: String, RawRepresentablePersistable {<br>case png<br>case jpg<br>case gif<br>case swift<br>// ...

typealias Post = V0_0_1.Post<br>typealias Attachment = V0_0_1.Attachment

enum Migration: SchemaMigrationPlan {<br>static let schemas: [VersionedSchema.Type] = [<br>V0_0_1.self

static let stages: [MigrationStage] = []

UI independent use:

func setupAndUseVein() throws {<br>// Optional: Setup keyring for Linux support<br>#if os(Linux)<br>Keyring.appIdentifier.withLock { $0 = "com.example.app" }<br>#endif

let container = try ModelContainer(<br>V0_0_1.self, // Your VersionedSchema<br>migration: Migration.self, // Your SchemaMigrationPlan<br>at: "path/to/db.sqlite3", // or nil for in memory<br>appID: "com.example.app" // The id of your app

try container.migrate()

let post = Post(title: "How to use Vein?", content: "It's very easy.")<br>try container.context.insert(post)

post.content = "What did I tell you?"

try container.context.save()

let posts = try container.context.fetchAll(#Predicate { post in<br>post.title.contains("Vein")<br>}) // gives back [post]

try container.context.delete(post)

Or with a @Query:

struct ContentView: View {<br>@Query(#Predicate { post in<br>post.title.contains("Swift")<br>})<br>var posts: [Post]

@Environment(\.modelContext) var context

var body: some View {<br>Button("Add post") {<br>do {<br>try context.insert(Post(title: "New Post", content: "..."))<br>try context.save()<br>} catch {<br>// Update some error state.<br>List(posts) { post in<br>Text(post.title)

If you know SwiftData, using Vein should be very easy to you.

The (hopefully) more interesting stuff:

The first thing you might have noticed is @LazyField. Vein eager loads fields by default, you can choose to explicitly declare that using @Field or, for data that's bigger and/or rarely accessed, you can apply @LazyField instead. It then will fetch the data for that specific field on first access.

The next, very obvious difference is that you are required to use a VersionedSchema and provide a SchemaMigrationPlan. It is good practice and since a database is such a critical piece, I wanted to enforce it. You don't have to migrate anything while you only have one version though (duh).

Aside from the very obvious API differences Vein is also very different under the hood. It's based on SQLite.swift or rather skip's swift-sqlcipher. Database level encryption is enabled by default on every platform and can be opted out of. The same core engine, SQLite+SQLCipher and SQLite.swift are used on all platforms, the only difference is how the database key is stored.

To store the database key, on Apple platforms the keychain via KeychainAccess is used, on Linux it's my own KeyringAccess library using SecretService and on Windows it used CredW. Due to the difficulty with android doing basically everything in their fake JVM you currently need to implement the...

post data self vein swift title

Related Articles