Load this after velar skill core and velar skill node. This brief contains
only the convention-based Server application framework. Node still owns route
syntax, HTTP/WebSocket transport, filesystem, process, and the low-level
velar/serve runtime.
A service installs and explicitly activates @velarscript/server, just as a
browser application activates @velarscript/web:
{
"dependencies": {
"@velarscript/server": "0.34.2"
}
}{
"formatVersion": 2,
"entry": "src/main.vel",
"outDir": "dist",
"publicDir": "public",
"extensions": ["@velarscript/server"],
"server": {
"configuration": "application.yml"
}
}The Server application extension composes the Node capability. Do not list both
extensions. Without @velarscript/server, velar/server is unavailable and
application configuration is not loaded.
The selected entry source must start the application inside @main; there is
no special exported startup binding. Source maps belong to the target-neutral
top-level build.sourceMaps switch. velar.json stores only the project-relative
configuration path. Host, port, request limits, database locations, and other
runtime values remain in that YAML or JSON file.
server.configuration is required and is the single configuration source.
The CLI's minimal template chooses root application.yml, but users may rename
or move it by editing velar.json; the framework never scans conventional
filenames.
YAML and JSON enter the same checked runtime-Type boundary. Files are bounded;
YAML is strict, rejects duplicate keys, and limits aliases. The declared path
must stay within the project and end in .yml, .yaml, or .json.
The framework-owned server section is:
server:
host: 127.0.0.1
port: 3000
maxBodyBytes: 16777216All three fields are optional. Their defaults are 127.0.0.1, 3000, and
16 MiB. port is an integer from 0 through 65535, and 0 means "any free
port", exactly as serve(app, port=0) means it; the port that was actually
bound is then readable as server.port on the value application(app)
returns. maxBodyBytes is 1 through 16777216. application(app) reads this section and creates the server; the
entry owns its lifetime explicitly:
import {application} from "velar/server"
import {run} from "velar/serve"
export server routes:
@get(p"/health") => {status: "ready"}
@main:
const server = await application(routes)
await run(server)
velar dev, velar serve, and the standalone velar build output use the
same declared configuration. CLI --host and Node --port overrides are not
a second configuration channel. A missing declared file fails closed, and it
fails in velar check with the same sentence velar build uses — whether a
declared file exists is a fact about the project, so it is judged where the
project is judged rather than only where output is written.
Declare the application-owned shape and load it through its Runtime Type:
import {configuration} from "velar/server"
type ServerSettings:
host: string
port: number
maxBodyBytes: number
type DatabaseSettings:
path: string
busyTimeoutMilliseconds: number
type ApplicationSettings:
server: ServerSettings
database: DatabaseSettings
export async def loadSettings() -> ApplicationSettings: return await configuration(ApplicationSettings)
configuration(Type, maxBytes=65536) returns Promise<Type> and always reads
the manifest-declared path.
The framework parses syntax; the application Runtime Type owns field names and
types, and application validation still owns domain ranges and invariants.
Environment variables may form an explicit deployment override layer, but
secrets do not belong in application configuration.
Node's security values own credential extraction, malformed-input rejection,
401 challenges, and OpenAPI descriptions. Server's authenticate composes one
of those descriptors with an application- or package-provided verifier:
import {authenticate} from "velar/server"
import {input, security} from "velar/serve"
type Principal:
subject: string
const exampleTokens: Map<string, Principal> = Map([
["example-test-token", {subject: "user-1"}],
])
async def verifyAccessToken(token: string) -> Principal?:
// A deployed application replaces this test map with an installed verifier.
return exampleTokens.get(token)
const currentPrincipal = authenticate(security.bearer(), verifyAccessToken)
export server accountRoutes:
@get(p"/me", principal=input.dependency(currentPrincipal)) => {
subject: principal.subject,
}
authenticate(credential, verify) accepts only a security.apiKey, basic,
bearer, oauth2, or openId descriptor. verify must return
Promise<Identity?>. It resolves once per request: null becomes the same
opaque not_authenticated 401 response and WWW-Authenticate challenge as a
missing credential, while a non-null value becomes the typed request Provider
result. A thrown verifier failure remains an opaque 500 because a key-service,
database, or network failure is not proof of an invalid credential.
The verified identity shape belongs to the application. JWT/JWK and OIDC
verification, password hashing, signed sessions, and vendor integrations are
explicit installed packages; user storage, tenant membership, roles,
permissions, revocation, and resource-level authorization remain application
policy. Do not place secrets in application.yml, invent a universal User
record, or turn authentication into new route syntax or @ roles.
database(connect, disconnect) creates an eager application-scoped
Provider<Connection>. It establishes one typed connection during application
startup, injects that same value into routes, and disconnects it during
application shutdown after admitted work has drained:
import {database} from "velar/server"
import {input} from "velar/serve"
type Connection:
name: string
close: () -> Promise<null>
const connection = database(
connect=async () => {name: "primary", close: async () => null},
disconnect=async value => await value.close(),
)
export server databaseRoutes:
@get(p"/database", value=input.dependency(connection)) => {name: value.name}
The framework owns only this connect/inject/disconnect lifecycle. Concrete
drivers, pools, database models, schemas, queries, migrations, transactions,
dialect behavior, retry rules, and credentials remain independently installed
application dependencies. Do not add SQLite, PostgreSQL, an ORM, or model
syntax to @velarscript/server or @velarscript/node.
Keep the physical socket and the application session separate. A declarative
@websocket route receives WebSocketConnection; pass it to
realtimeSession with the codec from the application's shared protocol
package:
import {Bytes} from "velar/binary"
import {RealtimeFailure, RealtimeFailureAction, RealtimePeer, realtimeSession} from "velar/realtime"
import {WebSocketConnection} from "velar/websocket"
type Command:
operation: string
type ServerEvent:
event: string
def decode(message: string | Bytes) -> Command:
if message is string: return Json.parse(message, Command)
throw Error("Binary commands are not supported")
def encode(event: ServerEvent) -> string | Bytes: return Json.stringify(event)
async def receive(command: Command, peer: RealtimePeer<ServerEvent>): await peer.send({event: command.operation})
async def failed(failure: RealtimeFailure, _peer: RealtimePeer<ServerEvent>):
print(failure.error)
return RealtimeFailureAction.close
async def serveSession(connection: WebSocketConnection):
await realtimeSession(
connection,
{decode, encode},
receive,
failed=failed,
options={maxQueuedMessages: 64, maxQueuedBytes: 1_048_576, drainTimeout: 5s},
)
The session runs one sequential inbound handler and one writer over a bounded
outbound mailbox. peer.send(event) waits for its own transport send;
peer.trySend(event) returns false instead of waiting when the mailbox is
full. opened may install a subscription and return an async cleanup function;
cleanup runs exactly once before the writer finishes draining. closed
receives the actual WebSocketClose code and reason.
RealtimeFailureAction.continue skips one failed decode or command and keeps
the session alive. Unrecoverable setup, transport, encode, and send failures
close the session. Route authentication, authorization, room membership,
subscription ownership, and application delivery semantics remain application
policy. Do not report a command as durable merely because send completed:
WebSocket is ordered but application delivery is still at-most-once unless the
shared protocol adds message IDs, acknowledgements, resume cursors, and
idempotent handling.
A service whose route table contains @websocket declarations may load the
same typed configuration and create the shared HTTP/WebSocket listener inside
@main:
import {configuration} from "velar/server"
import {listen, run} from "velar/websocket"
type ServerSettings:
host: string
port: number
maxBodyBytes: number
type Settings:
server: ServerSettings
@main:
const settings = await configuration(Settings)
const server = await listen({
host: settings.server.host,
port: settings.server.port,
http: routes,
origins: ["https://app.example.com"],
maxBodyBytes: settings.server.maxBodyBytes,
})
await run(server)
The entry module itself is the executable artifact; the declared application
configuration remains the single runtime authority. Each @websocket
RoutePattern owns its own path,
typed captures, admission inputs, and session handler, so this declarative mode
does not accept the listener's legacy single path option.
Use direct serve(app, port=0) in tests or embedded low-level adapters. Use
velar/server-test only from *.test.vel. Route, provider, transport,
Origin-admission, resource-limit, and shutdown semantics are documented in the
Node brief because Node owns those primitives.