Dartvel, home
Docs
Features
Studio
Cloud
Compared

Search the site

GitHub
pub.dev

GETTING STARTED

Getting started
Existing Flutter apps
Existing Native apps
Run on your phone

APP

UI and styling
Routing
State
Accessibility
Keyboard shortcuts
Localization
Devices and desktop
Native device access
Media, 3D and XR

DATA

Data models
Forms
Search
Sync and offline
Import and export
Change capture
Database
Cache
File storage
Images
Privacy and erasure

BACKEND

Backend functions
Auth and sessions
Authorization
Queues and jobs
Workers and memory
Notifications and mail
Outbound HTTP
AI
Webhooks
GraphQL and OpenAPI
API keys and OAuth
Multi-tenancy
Billing and commerce
Modules

OPERATIONS

Edge security
Secrets and environments
Monitoring
Releases

SHIPPING

Build targets
Telegram Mini Apps
Static web hosting
Servers and deploying

REFERENCE

Testing
CLI reference
Coding agents

DATA

Data models

Declare a data model once and get storage, a form, a table, an admin screen and public pages.

These are the models your data lives in. Dartvel has 3D models too, under Media, 3D and XR.

Every generated name is public, so your code never touches the private class.

ON THIS PAGE

Declare a data model

Data model options

Field annotations

Save, find and delete records

Use the generated form, table and page

Add an admin screen

Serve a public page per record

Catch stale writes

Keep and revert record history

Soft delete and restore

Search records

Protect sensitive fields

Status

Declare a data model

// lib/models/article.dart
import 'package:dartvel_core/dartvel.dart';

@DVModel(
  semantic: true,
  history: DVHistory(keep: Duration(days: 365)),
  softDelete: true,
  subject: DVSubject.field('authorId'),
  retain: DVRetention.indefinite,
)
class const _Article({
  required final String slug,
  @DVModel.pageTitle() required final String title,
  @DVModel.mainContent() required final String body,
  @DVModel.searchableField() required final String tags,
  required final bool published,
  required final String authorId,
  @DVModel.sensitiveField() required final String editorNotes,
});

Copy code to clipboard

Put it under lib/models, one model per file, as a private class with final fields.

The key is the slug field, else id, else the first String field.

Where records are stored is the framework's job. You read and write them through the model, on whichever database DATABASE_URL names.

Pick a name Dartvel does not use

Dartvel exports Get, Post, Put, Patch and Delete. A model with one of those names clashes with them in your code, so call it Article or BlogPost.

Data model options

generatePublicPages: false

What it does: No public page per record. Pages are on by default, at /articles/:slug

publicPathsResolver: fn

What it does: Your function lists the paths to render statically

history: DVHistory(...)

What it does: Keeps each version, with history() and revert()

softDelete: true

What it does: destroy() hides the record and restore() brings it back

version: false

What it does: Turns off the stale-write check

searchable: true

What it does: Gives the model Article.search

tenantScoped: true

What it does: Records belong to the current tenant

subject:, retain:

What it does: Who the data is about and how long to keep it

offline: DVConflict.lastWriteWins

What it does: Reads and writes a copy on the device and syncs it when the server can be reached

indexes: [DVIndex([...], unique: true)]

What it does: Asks the database for more indexes than the key

access: DVModelAccess(...)

What it does: Who may view, create, update and delete through the data API, where no @DVPolicy class says

Field annotations

@DVModel.pageTitle()

Effect: The title on its public page

@DVModel.mainContent()

Effect: The body of its public page

@DVModel.featuredImage()

Effect: The image for the page and link previews on pages generated from models

@DVModel.pageOrder(n)

Effect: Where the field sits on the page

@DVModel.hideFromPage()

Effect: Leaves the field off the page

@DVModel.searchableField()

Effect: Indexes the field for search

@DVModel.sensitiveField()

Effect: Kept out of logs, tables, pages and public JSON

@DVModel.model3dField()

Effect: A 3D asset with a poster

@DVModel.retain(years:, because:)

Effect: Keeps one field longer, with the reason

@DVModel.validate(min:, max:, minLength:, maxLength:, pattern:)

Effect: Checked by save() before anything is written, and by Studio

@DVModel.uniqueField()

Effect: A unique index, and a write that repeats a value is refused

Save, find and delete records

final Article article = Article(
  slug: 'hello-world',
  title: 'Hello, world',
  body: 'The first article.',
  tags: 'news',
  published: false,
  authorId: 'user-1',
  editorNotes: 'Check the title',
);
await article.save();

final List<Article> all = await Article.all();
final Article? found = await Article.find('hello-world');

final Article published = await found!.copyWith(published: true).save();
await published.destroy();

Copy code to clipboard

Model.all() and Model.find(key) read, model.save() creates or updates, and model.destroy() deletes.

copyWith keeps the version you read, so save() can spot a newer write.

Run dartvel db migrate after adding or changing a model. See Database.

Use the generated form, table and page

Widget articleEditor(Article article) => article.Form();

Widget articleTable(List<Article> articles) => Article.Table(articles);

Widget articlePage(Article article) => Article.Page.sync(article);

Copy code to clipboard

Article.Form() creates one and article.Form() edits that one. Saving is what the form does, and it checks the model's rules first. See Forms.

Article.Table(rows) sorts by column and skips sensitive fields.

Article.Page has .sync, .async, .signal and .fromId.

Add an admin screen

Functional

Class

@DVPage(title: 'Articles admin', policy: DVPolicies.viewAdmin)
Widget _articlesAdminPage(BuildContext context) => Article.Admin();

Copy code to clipboard

Article.Admin() lists, creates, edits and deletes records.

It checks the Article.create, Article.update and Article.delete policies.

dartvel admin generate writes admin pages to lib/pages/_dartvel_admin.

Serve a public page per record

Dartvel generates a public page for every record by default. To turn it off for a data model, set generatePublicPages: false on it.

The route comes from the model's name, in plural kebab-case, followed by the record's slug, or its id when the model has no slug: an Article is served at /articles/hello-world, and a BlogPost at /blog-posts/42.

dartvel build web renders the published records' pages statically and lists them in the sitemap.

Sensitive fields, and the field that names the record's subject, stay hidden. Only a viewer your viewSensitive policy allows sees them on the page.

They never appear in page titles, descriptions, structured data, the static build or the sitemap.

A record your view policy refuses gets a 404, the same as one that does not exist.

Users, sessions, tokens, audit logs and tenant data get no page unless you set generatePublicPages: true.

Decide who sees what

// lib/policies/article_policy.dart
import '../dartvel_client/dartvel_client.dart';

@DVPolicy(Article)
class ArticlePolicy {
  // Who may open an article's page. Anyone else gets a 404.
  bool view(DVSessionPrincipal? user, Article article) =>
      article.published || user?.userId == article.authorId;

  // Who also sees editorNotes and authorId on the page.
  bool viewSensitive(DVSessionPrincipal? user, Article article) =>
      user?.userId == article.authorId;
}

Copy code to clipboard

Render a record in your own page

Functional

Class

@DVPage(title: 'Article')
Widget _articlePage(BuildContext context) => Article.Page.fromId(
      context.dvParams['slug'] ?? 'hello-world',
      findById: (String slug) async => (await Article.find(slug))!,
    );

Copy code to clipboard

List the static paths yourself

@DVModel(publicPathsResolver: productPaths)
class const _Product({
  required final String slug,
  required final String name,
  required final bool published,
});

// Public, top level, and in the model's own file.
Future<List<String>> productPaths() async => <String>['starter-kit', 'pro-kit'];

Copy code to clipboard

Partial

Spec section: Generated Model Pages

Planned work and implementation limits

A favicon taken from a record's featured image is not built.

Catch stale writes

Each save checks the version you read. A newer write in between throws DVConflictError.

try {
  await article.save();
} on DVConflictError catch (conflict) {
  // Someone saved a newer version after you read this one.
  debugPrint('Stored version ${conflict.actualVersion}');
}

// Replace the stored row on purpose.
await Article.save(article, onConflict: DVConflict.lastWriteWins);

Copy code to clipboard

DVConflict also has serverWins, fieldMerge and DVConflict.resolver(...).

Keep and revert record history

// Needs @DVModel(history: DVHistory(...)).
final List<DVHistoryEntry> entries = await article.history();
for (final DVHistoryEntry entry in entries) {
  debugPrint('version ${entry.version} at ${entry.at}');
}
await article.revert(to: entries.first);

Copy code to clipboard

Partial

Spec section: Record History and Optimistic Concurrency

Planned work and implementation limits

Forms do not reload and merge on a conflict yet.

No Studio history view, scheduled prune or database-level atomic transactions.

Soft delete and restore

// Needs @DVModel(softDelete: true).
await article.destroy(); // hidden from find() and all()

final Article? deleted = await Article.withDeleted.find(article.slug);
final List<Article> everything = await Article.withDeleted.all();

await Article.restore(article.slug); // visible again

Copy code to clipboard

Search records

Article.useSearchProvider(DVInMemorySearchProvider<Article, ArticleFacets>(
  records: await Article.all(),
  document: (Article article) => '${article.title} ${article.body}',
));

final DVSearchResultPage<Article> page = await Article.search('dart', perPage: 10);

Copy code to clipboard

Set a provider before you query. With none, a query throws.

DVPostgresSearchProvider searches with Postgres full text.

Protect sensitive fields

A sensitive field needs subject: on the model, or the build stops with DV-PRIVACY-001.

encrypted: true encrypts a String field with keys from DARTVEL_FIELD_KEYS.

onErase: DVErase.anonymize keeps the record and blanks the field on erasure.

Partial

Spec section: Sensitive Model Fields

Planned work and implementation limits

Writes that skip the model, such as imports and backfills, store encrypted fields as plain text.

No key rotation, and showInAdmin does not gate anything yet.

Status

Built

Spec section: Models

PREVIOUS Media, 3D and XR Players, recorders, 3D scenes and spatial windows
NEXT Forms A create or edit form for every model
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom