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

BACKEND

Backend functions

Write a Dart function under lib/backend/functions and it is an HTTP endpoint.

Call it from your app as a typed function, with no client code to write.

ON THIS PAGE

Write a backend function

Call it from the app

Receive parameters

Use DVContext for the session and commit hooks

Serve a webhook at a fixed path

Watch a request move through its stages

Stream results with server-sent events

Undo partial work with a transaction

Move slow work to a job

Protect a function with a policy and MFA

Add middleware

CSRF protection is on for every function

Get an OpenAPI document for free

Status

Write a backend function

// lib/backend/functions/hello.get.dart is served at GET /api/hello.
import 'package:dartvel_core/dartvel.dart';

@DVBackendFunction()
Future<Map<String, Object?>> _hello(String name) async =>
    <String, Object?>{'greeting': 'Hello, $name'};

Copy code to clipboard

The file name sets the method: .get, .post, .put, .patch, .delete, .head or .options. No suffix means POST.

The path comes from the folders, with [id] as a parameter and index as the folder itself.

Every route is served under apiBasePath, /api by default.

Backend files import dartvel_core

The backend runs without Flutter. Import package:dartvel_core/dartvel.dart there, and leave DV and the generated client barrel to the app. The generated data models import Flutter, so a backend function cannot call Order.find() or order.save() yet.

Call it from the app

// In a page or anywhere in the app.
final Map<String, Object?> greeting = await hello(name: 'Ada');

final Map<String, Object?> order = await updateOrder(id: '42', status: 'shipped');

getTicks().listen((String tick) => DV.log(tick));

Copy code to clipboard

Parameters become required named arguments.

String, int, double, bool, List and Map results decode for you. Other types take a fromJson argument.

The raw call is there too, named after the route: getHello(...) returns the whole response.

Keep the two names apart

The raw call for hello.get.dart is getHello. A function named _getHello in that file would generate the same name, and the build stops and suggests another.

Receive parameters

Each parameter is read from the path, then the query string, then the body.

String, int, double and bool values are converted for you.

Use DVContext for the session and commit hooks

// lib/backend/functions/orders/[id].put.dart is served at PUT /api/orders/:id.
import 'package:dartvel_core/dartvel.dart';

@DVBackendFunction(
  policy: 'Order.update',
  mfa: DVMfa.recent(Duration(minutes: 15)),
)
Future<Map<String, Object?>> _updateOrder(
  DVContext context, // injected, and never sent by the client
  String id,
  String status,
) async {
  context.afterCommit(() => notifyCustomer(id));
  return <String, Object?>{
    'id': id,
    'status': status,
    'by': context.session?.userId,
  };
}

Copy code to clipboard

A first parameter of type DVContext is injected and left out of the client call.

context.session has the signed-in user, their tenant and claims.

context.afterCommit runs once the function succeeds. context.compensate runs if it fails.

Serve a webhook at a fixed path

// Served at POST /payments/webhook, outside /api.
@DVBackendFunction(rawPath: '/payments/webhook')
Future<Map<String, Object?>> _paymentWebhook(String event, String reference) async {
  await recordPayment(event, reference);
  return <String, Object?>{'received': true};
}

Copy code to clipboard

rawPath serves the function at exactly that path, outside apiBasePath. rawPathSuffix adds to the generated path instead, and you can use only one of the two.

A raw path reads no session cookie and asks for no CSRF token, so a payment provider can POST to it. An API key or OAuth token sent to it is still checked.

Both take plain segments. A path parameter in one stops the build, and so do two functions on the same address.

Watch a request move through its stages

@DVBackendFunction()
Future<Map<String, Object?>> _getExport(DVContext context, String id) async {
  final DVLifecycleSignal<DVRequestLifecycle> request = context.lifecycle.request;
  request.listen((DVRequestLifecycle state) {
    if (state == DVRequestLifecycle.failed) {
      DV.log('export $id failed', level: DVLogLevel.warn);
    }
  });
  return <String, Object?>{'id': id, 'stage': request.value.name};
}

Copy code to clipboard

context.lifecycle.request is read-only. You observe it, and the framework moves it.

Before your code runs, the body is read and the MFA and policy checks pass, with the CSRF check first on a generated path.

An error thrown by the function answers 500, runs its compensations and is recorded as a server crash.

Partial

Spec section: Backend Function Request Lifecycle

Planned work and implementation limits

Four stages are set today: received, executing, preparingResponse and failed.

transaction, authentication and rateLimit are not parameters of @DVBackendFunction yet.

The generated client does not send the binary flat-buffer format, though the server decodes it.

Stream results with server-sent events

// lib/backend/functions/stream/ticks.get.dart
import 'package:dartvel_core/dartvel.dart';

@DVBackendFunction()
Stream<String> _getTicks() => Stream<String>.periodic(
      const Duration(seconds: 1),
      (int i) => 'tick $i',
    ).take(10);

Copy code to clipboard

Return a Stream and the endpoint responds with text/event-stream. The client function returns a Stream you listen to.

Built

Spec section: Streaming Functions

Undo partial work with a transaction

@DVBackendFunction()
Future<Map<String, Object?>> _checkout(String orderId) =>
    DVTransactionRunner()((DVContext context) async {
      final String charge = await chargeCard(orderId);
      // Runs if anything later in this transaction throws.
      context.compensate(() => refund(charge));
      // Runs only once everything succeeded.
      context.afterCommit(() => sendReceipt(orderId));
      return <String, Object?>{'charge': charge};
    });

Copy code to clipboard

If the body throws, compensations run in reverse order and the error is rethrown.

In the app, the same call is DV.transaction((DVContext context) async { ... }).

A nested transaction joins the outer one unless you pass isolated: true.

Move slow work to a job

Dispatch a job and return straight away. A worker runs it with retries.

// lib/backend/functions/signup.post.dart
import 'package:dartvel_core/dartvel.dart';

import '../../dartvel_client/jobs.g.dart';

@DVBackendFunction()
Future<Map<String, Object?>> _signup(String userId) async {
  // The response goes out now. A worker sends the email.
  await SendWelcomeEmail(userId: userId).dispatch();
  return <String, Object?>{'queued': true};
}

Copy code to clipboard

See Queues and jobs for declaring the job and running workers.

Built

Spec section: Background and Durable Work

Implementation notes

background: and durable: are not parameters of @DVBackendFunction yet. Dispatch a job as shown here.

Protect a function with a policy and MFA

policy: 'Order.update' asks the OrderPolicy class. A refusal is 403, or 401 when nobody is signed in.

mfa: DVMfa.required, or DVMfa.recent(duration), needs a recent second factor.

A policy nothing answers stops the build.

See Authorization for writing the policy.

Add middleware

@DVUseMiddleware(<DVMiddlewareKey>[
  DVMiddlewares.auth,
  DVMiddlewares.rateLimit,
])
@DVBackendFunction()
Future<List<String>> _listReports() async => <String>['2026-08', '2026-09'];

Copy code to clipboard

Middleware runs in the order you list it. The keys that run are auth, tenant, rateLimit, requestLogging, securityHeaders, locale, idempotency, featureFlags, maintenance and csp.

Every response already carries X-Content-Type-Options, X-Frame-Options (SAMEORIGIN) and a Referrer-Policy, and HSTS when the server terminates TLS. A header your handler sets wins; pass serve() an empty securityHeaders map to send none.

bodyLimit and uploadLimit cap the request body at 1 MiB and 16 MiB, and answer 413 past it. tracing starts a trace for the request.

A key that does nothing yet, such as cors or cacheTags, stops the build and says why.

Set limits for the whole server

# pubspec.yaml
dartvel:
  server:
    maxBodyBytes: 2097152
    compression: true
    trustedProxies: [10.0.0.0/8]

Copy code to clipboard

CORS sits in the same server block. Every key there is checked when dartvel routes runs.

Partial

Spec section: Middleware

Planned work and implementation limits

Page middleware cannot preload data or set SEO context.

No layout, model or storage scopes, and no global middleware setting.

CSRF protection is on for every function

A POST, PUT, PATCH or DELETE to a generated function needs the x-dartvel-csrf-token header, or it gets 403.

The generated client sends the token for you, so there is nothing to add to your app.

Writing your own client? DV.CSRF.token() makes a token and DVCSRF.headerName names the header.

Built

Spec section: CSRF Protection

Get an OpenAPI document for free

The backend serves an OpenAPI 3.1 description of your functions at /api/openapi.json.

Status

Built

Spec section: Backend

Implementation notes

Functions are served over HTTP, and a Stream result over server-sent events. dartvel_shelf, the server the backend runs on, has served WebSockets since 0.9.2, but no backend function is given a WebSocket or polling transport yet.

PREVIOUS Privacy and erasure Export and erase a person's data
NEXT Auth and sessions Sign-in, second factor, sessions and account pages
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom