Skip to content
Orvanov0.2.0

Dart server quickstart

You build a small HTTP server with shelf whose GET /me reads a user’s access token, checks it with orvano_dart 0.2.0, and answers with that user. You also write a command that creates a user with a server API key. This is the pattern for any backend of yours that sits next to an app signing users in with Orvano. It takes about ten minutes.

The finished app is examples/dart-quickstart in the Orvano repo. Every file below comes from it, and CI runs it on every change.

If Orvano isn’t running on your machine yet, run these three commands. Run Orvano locally explains each one.

Terminal window
mkdir orvano-local && cd orvano-local
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/install" ghcr.io/orvanohq/orvano:0.2.0 install --local --yes
docker compose up -d --wait

You should see every service report Healthy, and the console open at http://localhost:7700.

  1. Open the setup link the installer printed and create the first admin. You land in your personal org.

    The setup page with a name, email, and password filled in, above the Create the first admin button.The setup page with a name, email, and password filled in, above the Create the first admin button.
  2. Select Create project, name it, and select Create project again. Setting it up takes a few seconds.

    The Create a project dialog with the name My app.The Create a project dialog with the name My app.
  3. On the project’s overview, copy the Project ID.

    The project overview, showing the project ID with a copy button.The project overview, showing the project ID with a copy button.
  4. Open API keys and select Create key. Name it, tick the scopes Users read and Users write, and select Create key. Copy the key it shows: you only see it once. A key can do only what its scopes allow, so this one can read and create users and nothing else.

    The Create an API key dialog with the name Quickstart server and the scopes Users read and Users write ticked.The Create an API key dialog with the name Quickstart server and the scopes Users read and Users write ticked.

Make a folder with this pubspec.yaml, then run dart pub get. It pins orvano_dart to 0.2.0, the version of the Orvano you’re running.

pubspec.yaml
dependencies:
orvano_dart: 0.2.0
shelf: ^1.4.2
The whole pubspec.yaml and analysis_options.yaml
pubspec.yaml
# The finished app of the Orvano Dart server quickstart: https://orvano.dev/docs/quickstarts/dart/
name: dart_quickstart
description: An HTTP server that checks an Orvano user's access token and answers with the user.
publish_to: none
environment:
sdk: ^3.8.0
dependencies:
orvano_dart: 0.2.0
shelf: ^1.4.2
dev_dependencies:
lints: ^6.1.0
analysis_options.yaml
include: package:lints/recommended.yaml

The server reads its settings from the environment, so the API key never sits in your code or your repo. Set them in the terminal you run the server from, with your project ID and the key you copied:

Terminal window
export ORVANO_ENDPOINT=http://localhost:7700
export ORVANO_PROJECT=your-project-id
export ORVANO_API_KEY=your-api-key

Make one client for the server. It sends the API key with every call, and it checks access tokens against your project’s public keys, which it caches for 10 minutes:

lib/orvano.dart
import 'dart:io';
import 'package:orvano_dart/orvano_dart.dart';
/// The server client. It sends your API key with every call and checks your
/// users' access tokens against the project's public keys.
final client = Client(
endpoint: _setting('ORVANO_ENDPOINT', fallback: 'http://localhost:7700'),
project: _setting('ORVANO_PROJECT'),
apiKey: _setting('ORVANO_API_KEY'),
);
/// Every server operation, grouped by service: `orvano.users.get(...)`.
final orvano = Orvano(client);
/// Reads a setting from the environment, so the API key never sits in code.
String _setting(String name, {String? fallback}) {
final value = Platform.environment[name] ?? fallback ?? '';
if (value.isEmpty) throw StateError('Set the $name environment variable.');
return value;
}

On a server, you create users with the API key, for example when you import them from another system. This command creates one; it runs on your machine and is never an open HTTP route:

bin/create_user.dart
/// Creates a user with the API key:
/// `dart run bin/create_user.dart <email> <password> [name]`.
Future<void> main(List<String> args) async {
if (args.length < 2) {
stderr.writeln(
'Usage: dart run bin/create_user.dart <email> <password> [name]',
);
exitCode = 64;
return;
}
try {
final user = await orvano.users.create(
CreateUserRequest(
email: args[0],
password: args[1],
name: args.length > 2 ? args[2] : null,
),
);
print('Created user ${user.id} (${user.email}).');
} on OrvanoException catch (error) {
stderr.writeln('Orvano refused: ${error.message} (${error.code})');
exitCode = 1;
} finally {
client.close();
}
}

Run it:

Terminal window
dart run bin/create_user.dart ada@example.com 'a long password' 'Ada Lovelace'

You should see Created user with the new user’s ID. The user is in the console too, on your project’s Users page.

GET /me reads the token from Authorization: Bearer <token>. verifyAccessToken checks its signature, its project, and its expiry, and answers with the user’s ID, so the server loads the user with users.get. A missing or bad token gets 401.

bin/server.dart
/// `GET /me`: checks the access token in `Authorization: Bearer <token>`, then
/// answers with the user it belongs to. A missing or bad token gets 401.
Future<Response> me(Request request) async {
final header = request.headers['authorization'] ?? '';
if (!header.startsWith('Bearer ')) return _unauthorized();
try {
// Checks the signature, the project, and the expiry, without a call to
// Orvano once the project's public keys are cached.
final token = await client.verifyAccessToken(
header.substring('Bearer '.length),
);
final user = await orvano.users.get(token.userId);
return Response.ok(
jsonEncode({'id': user.id, 'email': user.email, 'name': user.name}),
headers: {'content-type': 'application/json'},
);
} on OrvanoException catch (error) {
// invalid_token or token_expired; anything else is a real failure.
if (error.status != 401) rethrow;
return _unauthorized();
}
}
Response _unauthorized() => Response.unauthorized(
jsonEncode({'error': 'Send a valid access token.'}),
headers: {'content-type': 'application/json', 'www-authenticate': 'Bearer'},
);
The whole bin/server.dart
bin/server.dart
import 'dart:convert';
import 'dart:io';
import 'package:dart_quickstart/orvano.dart';
import 'package:orvano_dart/orvano_dart.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
Future<void> main() async {
// Dart creates `client` the first time it's used. Using it here reads the
// settings now, so a missing one stops the server before it listens.
print('Orvano project: ${client.project}');
final server = await shelf_io.serve(
_route,
InternetAddress.loopbackIPv4,
3001,
);
print('Listening on http://localhost:${server.port}');
}
Future<Response> _route(Request request) async {
if (request.method == 'GET' && request.url.path == 'me') return me(request);
return Response.notFound('Not found');
}
/// `GET /me`: checks the access token in `Authorization: Bearer <token>`, then
/// answers with the user it belongs to. A missing or bad token gets 401.
Future<Response> me(Request request) async {
final header = request.headers['authorization'] ?? '';
if (!header.startsWith('Bearer ')) return _unauthorized();
try {
// Checks the signature, the project, and the expiry, without a call to
// Orvano once the project's public keys are cached.
final token = await client.verifyAccessToken(
header.substring('Bearer '.length),
);
final user = await orvano.users.get(token.userId);
return Response.ok(
jsonEncode({'id': user.id, 'email': user.email, 'name': user.name}),
headers: {'content-type': 'application/json'},
);
} on OrvanoException catch (error) {
// invalid_token or token_expired; anything else is a real failure.
if (error.status != 401) rethrow;
return _unauthorized();
}
}
Response _unauthorized() => Response.unauthorized(
jsonEncode({'error': 'Send a valid access token.'}),
headers: {'content-type': 'application/json', 'www-authenticate': 'Bearer'},
);

Start the server with dart run bin/server.dart. You should see Listening on http://localhost:3001.

In another terminal (with the same settings), sign in as the user you created to get an access token, the way an app’s sign in would, and call /me with it:

Terminal window
curl -s http://localhost:7700/v1/account/sessions/password \
-H "X-Orvano-Project: $ORVANO_PROJECT" -H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"a long password"}'
# Copy session.accessToken from the answer, then:
TOKEN=paste-the-access-token-here
curl -i http://localhost:3001/me -H "Authorization: Bearer $TOKEN"
  1. You should see 200 OK and the user’s ID, email, and name.
  2. Call /me again without the Authorization header. You should see 401 Unauthorized.
  3. Change one character of the token and call it again. You should see 401 again: the signature no longer matches.

Access tokens last 15 minutes. In a real app, the client SDK refreshes the session and sends a fresh token with each call.