@harperfast/template-vue-ssr-studio 1.12.12 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +684 -239
  2. package/.agents/skills/harper-best-practices/SKILL.md +2 -1
  3. package/.agents/skills/harper-best-practices/rules/creating-a-fabric-account-and-cluster.md +7 -4
  4. package/.agents/skills/harper-best-practices/rules/creating-harper-apps.md +71 -4
  5. package/.agents/skills/harper-best-practices/rules/custom-resources.md +63 -29
  6. package/.agents/skills/harper-best-practices/rules/deploying-from-ci.md +358 -0
  7. package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +132 -103
  8. package/.agents/skills/harper-best-practices/rules/load-env.md +37 -49
  9. package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +71 -57
  10. package/.agents/skills/harper-best-practices/rules.manifest.yaml +184 -8
  11. package/README.md +0 -48
  12. package/agent/skills/harper-best-practices/AGENTS.md +684 -239
  13. package/agent/skills/harper-best-practices/SKILL.md +2 -1
  14. package/agent/skills/harper-best-practices/rules/creating-a-fabric-account-and-cluster.md +7 -4
  15. package/agent/skills/harper-best-practices/rules/creating-harper-apps.md +71 -4
  16. package/agent/skills/harper-best-practices/rules/custom-resources.md +63 -29
  17. package/agent/skills/harper-best-practices/rules/deploying-from-ci.md +358 -0
  18. package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +132 -103
  19. package/agent/skills/harper-best-practices/rules/load-env.md +37 -49
  20. package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +71 -57
  21. package/agent/skills/harper-best-practices/rules.manifest.yaml +184 -8
  22. package/package.json +1 -1
  23. package/skills-lock.json +1 -1
@@ -1605,7 +1605,7 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
1605
1605
 
1606
1606
  #### When to Use
1607
1607
 
1608
- Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed resource or when a specific URL shape is required.
1608
+ Apply this rule when you need to create custom HTTP endpoints, wrap external APIs, or register routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed schema, or when you need fine-grained control over URL shape and path parameters.
1609
1609
 
1610
1610
  #### How It Works
1611
1611
 
@@ -1615,7 +1615,13 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1615
1615
  import { tables, Resource } from 'harper';
1616
1616
  ```
1617
1617
 
1618
- 2. **Define a class that `extends Resource`**: Implement HTTP methods as `static` methods. Each method receives a `target` object.
1618
+ CommonJS alternative:
1619
+
1620
+ ```javascript
1621
+ const { tables, Resource } = require('harper');
1622
+ ```
1623
+
1624
+ 2. **Define a class that `extends Resource`**: Use `export class` so Harper exposes it as an endpoint. Implement HTTP methods as `static` methods on the class.
1619
1625
 
1620
1626
  ```javascript
1621
1627
  export class CustomEndpoint extends Resource {
@@ -1627,7 +1633,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1627
1633
  }
1628
1634
  ```
1629
1635
 
1630
- 3. **Use `async` static methods to call external services**: Return or forward the response directly.
1636
+ 3. **Use `static` async methods to call external services**: Each HTTP verb maps to a `static` method of the same lowercase name.
1631
1637
 
1632
1638
  ```javascript
1633
1639
  export class MyExternalData extends Resource {
@@ -1645,24 +1651,20 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1645
1651
  }
1646
1652
  ```
1647
1653
 
1648
- 4. **Export the class to expose it as an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
1654
+ 4. **Control the URL by choosing the export form**: The shape of the export determines the resulting URL path.
1649
1655
 
1650
1656
  | Export form | URL | Notes |
1651
1657
  | ------------------------------------------- | --------------- | --------------------------------------------------------------- |
1652
- | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. Case-sensitive. |
1658
+ | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment; case-sensitive. |
1653
1659
  | `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
1654
1660
  | `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
1655
1661
  | `export { Foo as '/widget/:id' }` | `/widget/:id` | Rename the export to set the path directly. |
1656
1662
  | `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
1657
- | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
1663
+ | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration; useful when the path is dynamic. |
1658
1664
 
1659
- 5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
1660
-
1661
- ```javascript
1662
- server.resources.set('my-path', Foo);
1663
- ```
1665
+ URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
1664
1666
 
1665
- 6. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
1667
+ 5. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
1666
1668
 
1667
1669
  ```javascript
1668
1670
  export class Widget extends Resource {
@@ -1672,13 +1674,36 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1672
1674
  return { id: target.id, action: target.action };
1673
1675
  }
1674
1676
  }
1677
+
1678
+ export class Files extends Resource {
1679
+ // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
1680
+ static path = '/files/*rest';
1681
+ static get(target) {
1682
+ return { path: target.rest };
1683
+ }
1684
+ }
1675
1685
  ```
1676
1686
 
1677
- 7. **Resolve path precedence correctly**: Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats a `:param`, which beats a `*` wildcard, compared left to right.
1687
+ - A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
1688
+ - A leading `./` or bare name resolves relative to the component directory.
1689
+ - A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment.
1690
+ - `static path` takes precedence over the export name.
1691
+ - Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats `:param`, which beats `*`, compared left to right.
1692
+
1693
+ 6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
1694
+
1695
+ ```javascript
1696
+ server.resources.set('my-path', Foo);
1697
+ ```
1698
+
1699
+ 7. **Optionally use a resource as a cache source for a local table**: Call `sourcedFrom` on the target table.
1700
+ ```javascript
1701
+ tables.MyCache.sourcedFrom(MyExternalData);
1702
+ ```
1678
1703
 
1679
1704
  #### Examples
1680
1705
 
1681
- **Wrapping an external API and using it as a cache source:**
1706
+ **Custom endpoint with external API wrapping:**
1682
1707
 
1683
1708
  ```javascript
1684
1709
  import { tables, Resource } from 'harper';
@@ -1701,9 +1726,19 @@ export class MyExternalData extends Resource {
1701
1726
  tables.MyCache.sourcedFrom(MyExternalData);
1702
1727
  ```
1703
1728
 
1704
- **Catch-all wildcard path:**
1729
+ **Path parameters with `static path`:**
1705
1730
 
1706
1731
  ```javascript
1732
+ import { Resource } from 'harper';
1733
+
1734
+ export class Widget extends Resource {
1735
+ // GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
1736
+ static path = '/widget/:id/action/:action';
1737
+ static get(target) {
1738
+ return { id: target.id, action: target.action };
1739
+ }
1740
+ }
1741
+
1707
1742
  export class Files extends Resource {
1708
1743
  // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
1709
1744
  static path = '/files/*rest';
@@ -1713,27 +1748,26 @@ export class Files extends Resource {
1713
1748
  }
1714
1749
  ```
1715
1750
 
1716
- **Root-relative fixed route:**
1751
+ **Programmatic registration:**
1717
1752
 
1718
1753
  ```javascript
1719
- export class AcmeChallenge extends Resource {
1720
- static path = '/.well-known/acme-challenge/:token';
1754
+ import { Resource } from 'harper';
1755
+
1756
+ export class Foo extends Resource {
1721
1757
  static get(target) {
1722
- return { token: target.token };
1758
+ return { ok: true };
1723
1759
  }
1724
1760
  }
1761
+
1762
+ server.resources.set('my-path', Foo);
1725
1763
  ```
1726
1764
 
1727
1765
  #### Notes
1728
1766
 
1729
- - URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
1730
- - A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
1731
- - A leading `./` or a bare name in `static path` resolves relative to the component directory.
1732
- - A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
1733
- - `static path` takes precedence over the export name when both are present.
1734
- - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as URI templates.
1735
- - When a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
1736
- - Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
1767
+ - Avoid conflicting exports between the schema and the JavaScript implementation when a resource `extends` an existing table.
1768
+ - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as `{param}` URI templates.
1769
+ - When bundling for SSR with a tool like Vite, keep `harper` external so it resolves to the runtime: `ssr: { external: ['harper'] }`.
1770
+ - For components in their own directory, run `npm link harper` to ensure typings match the running installation.
1737
1771
 
1738
1772
  ### 3.2 Extending Tables
1739
1773
 
@@ -1833,25 +1867,31 @@ export class MyTable extends tables.MyTable {
1833
1867
 
1834
1868
  ### 3.3 Programmatic Table Requests
1835
1869
 
1836
- Instructions for the agent to interact with Harper tables programmatically using the `tables` object, the Query API, and transactions.
1870
+ Instructions for the agent to follow when interacting with Harper tables programmatically using the `tables` object.
1837
1871
 
1838
1872
  #### When to Use
1839
1873
 
1840
- Apply this rule when writing server-side Harper component code that reads from or writes to tables directly — for example, in HTTP handlers, background jobs, timers, or SSR render functions. Use it whenever you need to construct queries with `conditions`, `sort`, `select`, `limit`, or `offset`, or when you need explicit transaction control via `transaction()`.
1874
+ Apply this rule when writing server-side Harper component code that reads from or writes to tables using the programmatic API — for example, in HTTP handlers, background jobs, timers, or SSR entry points. Use it whenever you need to construct queries with `conditions`, `sort`, `select`, pagination, or transactions outside of the REST layer.
1841
1875
 
1842
1876
  #### How It Works
1843
1877
 
1844
- 1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a named property of `tables`. Each property is the table class implementing the Resource API.
1878
+ 1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a property of `tables`. Each property is the table class implementing the Resource API.
1845
1879
 
1846
1880
  ```javascript
1847
1881
  import { tables, transaction } from 'harper';
1848
1882
  const { Product } = tables;
1849
- // equivalent to: databases.data.Product
1883
+ // same as: databases.data.Product
1884
+ ```
1885
+
1886
+ For components in their own directory, run:
1887
+
1888
+ ```bash
1889
+ npm link harper
1850
1890
  ```
1851
1891
 
1852
- For standalone components, run `npm link harper` so imports resolve to the live runtime.
1892
+ All installed components have `harper` automatically linked.
1853
1893
 
1854
- 2. **Define your schema with `@table`**: Tables must be declared in `schema.graphql`. Mark attributes you intend to sort or filter on with `@indexed`.
1894
+ 2. **Define your schema with `@table`**: Every type you want to access via `tables` must carry the `@table` directive. Mark queryable fields with `@indexed`.
1855
1895
 
1856
1896
  ```graphql
1857
1897
  type Product @table {
@@ -1861,7 +1901,7 @@ Apply this rule when writing server-side Harper component code that reads from o
1861
1901
  }
1862
1902
  ```
1863
1903
 
1864
- 3. **Query records with `search(`**: Pass a Query object to `search(`. Iterate results with `for await`.
1904
+ 3. **Query records using `search(`**: Pass a Query object to `search()`. The query is an async iterable.
1865
1905
 
1866
1906
  ```javascript
1867
1907
  const query = {
@@ -1872,7 +1912,7 @@ Apply this rule when writing server-side Harper component code that reads from o
1872
1912
  }
1873
1913
  ```
1874
1914
 
1875
- 4. **Build `conditions`**: Each condition object supports these properties:
1915
+ 4. **Build `conditions`**: Each condition object supports the following properties:
1876
1916
 
1877
1917
  | Property | Description |
1878
1918
  | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -1882,53 +1922,60 @@ Apply this rule when writing server-side Harper component code that reads from o
1882
1922
  | `conditions` | Nested conditions array |
1883
1923
  | `operator` | `and` (default) or `or` for the nested `conditions` |
1884
1924
 
1885
- 5. **Control result shape with `select`**: Pass an array of property names, a single string, or nested objects for relationships.
1925
+ 5. **Apply `select` to shape results**: Pass an array of property names, a string for a single property, or nested objects for relationships.
1886
1926
 
1887
1927
  ```javascript
1888
- // Scalar fields only
1928
+ // Flat select
1889
1929
  Product.search({ select: ['name', 'price'] });
1890
1930
 
1891
- // Partial related record
1931
+ // Nested relationship select
1892
1932
  Book.get({ id: 42, select: ['id', 'title', { name: 'author', select: ['name'] }] });
1893
1933
  ```
1894
1934
 
1895
1935
  Special `select` values: `$id`, `$updatedtime`, `$distance`.
1896
1936
 
1897
- 6. **Paginate with `limit` and `offset`**:
1898
-
1899
- ```javascript
1900
- Product.search({ conditions: [...], limit: 20, offset: 40 });
1901
- ```
1937
+ 6. **Apply `sort`**: Harper uses an index to provide sort order.
1902
1938
 
1903
- 7. **Sort with `sort`**: The `sort` object accepts `attribute`, `descending` (default `false`), and `next` for tie-breaking. Harper uses an index for sort order — the sort `attribute` must be `@indexed`, **or** at least one entry in `conditions` must be present.
1939
+ | Property | Description |
1940
+ | ------------ | ---------------------------------------------------------- |
1941
+ | `attribute` | Property name (or array for chained relationship property) |
1942
+ | `descending` | Sort descending if `true` (default: `false`) |
1943
+ | `next` | Secondary sort to resolve ties (same structure) |
1944
+ - If the sort `attribute` is `@indexed`, no condition is required.
1945
+ - If the sort `attribute` is not indexed, at least one entry in `conditions` (on **any** attribute) is required.
1946
+ - Sorting by a non-indexed attribute with zero conditions raises:
1947
+ > `HdbError: <attribute> is not indexed and not combined with any other conditions`
1948
+ - The bare `@primaryKey` is treated as not indexed for sort purposes. To iterate in primary-key order, add an open-ended condition:
1904
1949
 
1905
- Sorting by a non-indexed attribute with zero conditions raises:
1950
+ ```javascript
1951
+ Product.search({
1952
+ conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
1953
+ sort: { attribute: 'id' },
1954
+ });
1955
+ ```
1906
1956
 
1907
- > `HdbError: <attribute> is not indexed and not combined with any other conditions`
1957
+ - Pass `allowFullScan: true` to permit an unconditional ordered scan, or omit `sort` entirely to iterate without an index requirement.
1908
1958
 
1909
- Note: `@primaryKey` alone is treated as not indexed for sort purposes. To scan the whole table in primary-key order, add an open-ended condition:
1959
+ 7. **Paginate with `limit` and `offset`**:
1910
1960
 
1911
1961
  ```javascript
1912
- Product.search({
1913
- conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
1914
- sort: { attribute: 'id' },
1915
- });
1962
+ Product.search({ conditions: [...], limit: 20, offset: 40 });
1916
1963
  ```
1917
1964
 
1918
- Alternatively, pass `allowFullScan: true` to permit an unconditional ordered scan, or omit `sort` entirely to iterate without an index requirement.
1919
-
1920
- 8. **Debug query planning with `explain` and `enforceExecutionOrder`**:
1965
+ 8. **Debug with `explain` and `enforceExecutionOrder`**:
1921
1966
  - `explain: true` — returns conditions reordered as Harper will execute them.
1922
- - `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling automatic re-ordering.
1967
+ - `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling Harper's automatic re-ordering.
1923
1968
 
1924
- 9. **Use `addTo` for concurrent-safe numeric updates**: `addTo` uses CRDT incrementation, safe across threads and nodes.
1969
+ 9. **Use `addTo` for concurrent-safe increments**: `addTo(property, value)` uses CRDT incrementation, safe across threads and nodes.
1925
1970
 
1926
1971
  ```javascript
1927
- const record = await Product.update(32);
1928
- record.addTo('quantity', -1);
1972
+ static async post(target, data) {
1973
+ const record = await this.update(target.id);
1974
+ record.addTo('quantity', -1);
1975
+ }
1929
1976
  ```
1930
1977
 
1931
- 10. **Wrap background work in `transaction()`**: HTTP handlers get a transaction automatically. Use `transaction()` explicitly for timers, background jobs, or any code outside a request context. Always `await` the call and `catch` errors.
1978
+ 10. **Wrap background work in `transaction()`**: Harper auto-starts transactions for HTTP handlers. Use `transaction()` explicitly for timers, background jobs, or any code outside a natural transaction context.
1932
1979
 
1933
1980
  ```javascript
1934
1981
  await transaction(async (txn) => {
@@ -1940,18 +1987,18 @@ Apply this rule when writing server-side Harper component code that reads from o
1940
1987
 
1941
1988
  The `txn` object exposes:
1942
1989
 
1943
- | Member | Description |
1944
- | --------------------- | ------------------------------------------------------ |
1945
- | `commit()` | Commits the current transaction |
1946
- | `abort()` | Aborts and resets the transaction |
1947
- | `resetReadSnapshot()` | Resets the read snapshot to the latest committed state |
1948
- | `timestamp` | Timestamp associated with the current transaction |
1990
+ | Member | Type | Description |
1991
+ | --------------------- | --------------- | ------------------------------------------------------ |
1992
+ | `commit()` | `() => Promise` | Commits the current transaction |
1993
+ | `abort()` | `() => void` | Aborts the transaction and resets it |
1994
+ | `resetReadSnapshot()` | `() => void` | Resets the read snapshot to the latest committed state |
1995
+ | `timestamp` | `number` | Timestamp associated with the current transaction |
1949
1996
 
1950
- **Atomicity**: all tables in the same database share one transactional context — writes across multiple tables commit atomically. Tables in different databases each get their own transaction with no cross-database atomicity guarantee.
1997
+ On normal callback completion the transaction commits automatically. If the callback throws, the transaction is aborted.
1951
1998
 
1952
- If `transaction()` is called with a context that already has an active transaction, it reuses that transaction — safe to call defensively.
1999
+ 11. **Understand atomicity**: Transactions span a single database. All tables within the same database share one transactional context — reads return a consistent snapshot and writes across multiple tables commit atomically. Cross-database operations get separate transactions with no cross-database atomicity guarantee.
1953
2000
 
1954
- 11. **Keep `harper` external when bundling for SSR**: In `vite.config`, mark `harper` as external so it resolves to the runtime rather than being bundled.
2001
+ 12. **Keep `harper` external when bundling for SSR**: In `vite.config`, mark `harper` as external so it resolves to the runtime rather than being bundled.
1955
2002
 
1956
2003
  ```javascript
1957
2004
  // vite.config
@@ -1962,7 +2009,7 @@ Apply this rule when writing server-side Harper component code that reads from o
1962
2009
 
1963
2010
  #### Examples
1964
2011
 
1965
- ##### Full CRUD sequence
2012
+ **Full CRUD flow:**
1966
2013
 
1967
2014
  ```javascript
1968
2015
  import { tables } from 'harper';
@@ -1974,19 +2021,18 @@ const created = await Product.create({ name: 'Shirt', price: 9.5 });
1974
2021
  // Patch
1975
2022
  await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
1976
2023
 
1977
- // Retrieve by primary key
2024
+ // Get by primary key
1978
2025
  const record = await Product.get(created.id);
1979
2026
 
1980
- // Query with conditions
1981
- const query = {
2027
+ // Search with conditions
2028
+ for await (const record of Product.search({
1982
2029
  conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
1983
- };
1984
- for await (const record of Product.search(query)) {
2030
+ })) {
1985
2031
  // process record
1986
2032
  }
1987
2033
  ```
1988
2034
 
1989
- ##### Nested conditions with `or`
2035
+ **Nested conditions with `or`:**
1990
2036
 
1991
2037
  ```javascript
1992
2038
  Product.search({
@@ -2003,13 +2049,13 @@ Product.search({
2003
2049
  });
2004
2050
  ```
2005
2051
 
2006
- ##### Chained attribute reference (join/relationship)
2052
+ **Chained attribute reference (join):**
2007
2053
 
2008
2054
  ```javascript
2009
2055
  Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
2010
2056
  ```
2011
2057
 
2012
- ##### Background job with `transaction()`
2058
+ **Background job with `transaction()`:**
2013
2059
 
2014
2060
  ```javascript
2015
2061
  import { isMainThread } from 'node:worker_threads';
@@ -2037,7 +2083,7 @@ if (isMainThread) {
2037
2083
  }
2038
2084
  ```
2039
2085
 
2040
- ##### SSR render with `tables`
2086
+ **SSR with Harper data:**
2041
2087
 
2042
2088
  ```typescript
2043
2089
  import { tables } from 'harper';
@@ -2050,10 +2096,12 @@ export async function render(url: string): Promise<string> {
2050
2096
 
2051
2097
  #### Notes
2052
2098
 
2053
- - `tables` and `databases` do **not** automatically apply role permissions — calls run in a trusted server-side context. Apply your own authorization controls before exposing results.
2054
- - Destructive operations (`update`, `patch`, `delete`) act on live data and are not easily reversible. Scope them with specific `conditions` and validate the affected set before writing.
2055
- - `tables` is the same live, process-wide object whether accessed as a global or via `import { tables } from 'harper'`. A record written through one component is immediately visible to every other.
2056
- - Run `npm link harper` for components in their own directory to ensure typings match the running installation.
2099
+ - `tables` and `databases` calls run in a trusted server-side context and do **not** automatically apply the target table's role permissions.
2100
+ - Programmatic `update`, `patch`, and `delete` calls operate directly on stored data. Scope destructive operations with specific `conditions`, validate the affected set before writing, and gate them behind authorization controls.
2101
+ - If `transaction()` is called with a context that already has an active transaction, it reuses that transaction — making it safe to call defensively.
2102
+ - Always `await` the `transaction()` call and `catch` errors when outside a request context; an unawaited call means a failed write is never observed.
2103
+ - Guard against timer overlap: if a job can outlast its interval, use a `running` flag to skip a tick rather than opening two transactions over the same rows.
2104
+ - CommonJS is also supported: `const { tables, Resource } = require('harper');`
2057
2105
 
2058
2106
  ### 3.4 TypeScript Type Stripping in Harper
2059
2107
 
@@ -2272,22 +2320,25 @@ await fetch('http://localhost:9926/JokeCache/1', {
2272
2320
 
2273
2321
  ### 4.1 Deploying to Harper Fabric
2274
2322
 
2275
- Instructions for the agent to follow when deploying a Harper application to a remote Harper Fabric cloud cluster.
2323
+ Instructions for the agent to follow when deploying a Harper application to a Harper Fabric cluster, or any remote Harper instance, with the `harper` CLI, and when checking, staging, or going back to a release.
2276
2324
 
2277
2325
  #### When to Use
2278
2326
 
2279
- Apply this rule when deploying a Harper application to a remote Harper instance or Fabric cluster, including first-time deploys, redeployments, rollbacks, and CI/CD pipeline deployments. Also apply it when provisioning credentials for private repository deploys. See [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md) for setting up the cluster before deploying.
2327
+ Apply this rule when deploying to a remote cluster, choosing how it restarts, reading a deploy's `certification`, staging a release to make live later, or rolling back to a previous release. To deploy from CI with no stored credential, see [deploying-from-ci.md](deploying-from-ci.md). To set up the cluster first, see [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md).
2280
2328
 
2281
2329
  #### How It Works
2282
2330
 
2283
- 1. **Authenticate against the remote cluster**: Run `harper login` once, pointing at the cluster's Application URL (found on the cluster's **Config → Overview** page). The CLI stores the token and writes `HARPER_CLI_TARGET` to a local `.env`.
2331
+ 1. **Log in to the cluster once**: Pass the cluster's **Application URL**, from its **Config → Overview** page, to `harper login`. The CLI stores the token, so later commands don't repeat credentials. If the current directory already has a `.env` file that sets no target, it also appends `HARPER_CLI_TARGET` to it.
2284
2332
 
2285
2333
  ```bash
2286
2334
  harper login <Application URL>
2287
2335
  # Provide cluster username and password when prompted
2288
2336
  ```
2289
2337
 
2290
- 2. **Deploy the application**: After login, run `harper deploy` without repeating credentials. Use `restart=true` and `replicated=true` for production deploys.
2338
+ - For CI/CD on GitHub Actions, use workload identity (OIDC) instead, so the pipeline stores no Harper credential. From v5.4.0, `harper deploy setup=true provider=github-actions` sets it up in one command: see [deploying-from-ci.md](deploying-from-ci.md). Elsewhere, give the pipeline a refresh token from `harper login --for-ci` rather than a password.
2339
+ - For a one-off command, `auth_username=` and `auth_password=` also work, and take precedence over environment variables and saved login tokens. Don't use them in production.
2340
+
2341
+ 2. **Deploy with a restart**: Run `harper deploy`, with `restart=true` or `restart=rolling` so the release is certified before it serves (step 3).
2291
2342
 
2292
2343
  ```bash
2293
2344
  harper deploy \
@@ -2298,100 +2349,123 @@ Apply this rule when deploying a Harper application to a remote Harper instance
2298
2349
  replicated=true
2299
2350
  ```
2300
2351
 
2301
- 3. **Choose a package source**: Set the `package` parameter to any valid npm dependency value. Options:
2352
+ - On Harper Pro and Fabric, a deploy goes to every node in the cluster unless you pass `replicated=false`. Harper core on its own does not replicate.
2353
+ - `project` defaults to the current directory's name for a directory deploy, or is derived from the package for a package deploy.
2354
+ - A bare `target` host defaults to `https://<host>:9925`.
2355
+ - Pass `json=true` to print the result as JSON instead of YAML.
2356
+ - Every result carries a `deployment_id`, staged or not. It names this release when you go back to it later (step 4).
2302
2357
 
2303
- | Source | Example value |
2304
- | ----------------------- | ------------------------------------------------------------ |
2305
- | Current local directory | Omit `package` |
2306
- | npm package | `package="@harperdb/status-check"` |
2307
- | GitHub (shorthand) | `package="HarperFast/status-check"` |
2308
- | GitHub (URL) | `package="https://github.com/HarperFast/status-check"` |
2309
- | Private repo (SSH) | `package="git+ssh://git@github.com:HarperDB/secret-app.git"` |
2310
- | Tarball | `package="https://example.com/application.tar.gz"` |
2358
+ 3. **Choose the restart, and read `certification`**: `restart=true` restarts Harper after deploying. On a cluster, use `restart=rolling`, the staggered, zero-downtime restart: the command exits `0` once the node you called has taken the release and started the rolling restart's job, and returns its `restartJobId`.
2311
2359
 
2312
- When using git tags, use the `semver` directive:
2360
+ Both certify the release in a canary worker before rolling it out. The canary replaces worker 0 and loads every component, but is held out of traffic until it reports whether the deployed component loaded. Until then, the workers already running keep serving the previous release. A deploy that succeeds, and is not staged, reports the result in `certification`:
2313
2361
 
2314
- ```
2315
- HarperFast/application-template#semver:v1.0.0
2316
- ```
2362
+ | `certification` | Meaning |
2363
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2364
+ | `certified` | The canary loaded the release, and the rollout went on. |
2365
+ | `uncertified` | The rollout went on unchecked: the component ran nothing as it loaded (`loadComponent: dev-only`, an absent `if-installed` component, or safe mode), or no worker was replaced. |
2366
+ | `unavailable` | The deploy restarted unchecked: no worker on this node loads the component, its `isolated` setting changed, it was already live, or it was deployed from a local directory (`package: file:<dir>`). |
2367
+ | `not-requested` | The deploy did not restart. |
2368
+ - **A rejected release fails the deploy.** If the canary throws while loading the component, exits, or doesn't report within a minute, the deploy fails with `400`. The release it replaced is made live again, and nothing is replicated to other nodes. The error's `certification` is an object whose `status` is `rejected`, or `interrupted` for a restart that stopped before the canary decided.
2369
+ - **A release with nothing to go back to fails closed.** A rejected first deploy, or one whose previous release was not kept, stays on disk, but no worker on that node loads it, even after Harper restarts. `get_status` reports the component as failed. Deploy a fix, or once the cause is fixed, activate the same `deployment_id` again with `restart`.
2370
+ - **A rolling deploy exits before every node has taken the release.** The other nodes stage it, and the job activates it on each in turn, each with a canary of its own. A node that rejects it keeps the previous release, or fails closed if it had none. Nothing is rolled back on the others, and the job fails naming each node that did not activate it. The deployment record reports `success` while the job runs and after it fails, so poll the job instead: `harper get_job id=<restartJobId>` ends `COMPLETE`, or `ERROR` with a `message` listing each node's outcome under `activated`.
2371
+ - **Poll the node that received the deploy.** A job is recorded only on the node that ran it, so any other node answers `get_job` with an empty array. A role with an `operations` allowlist, or a token with an `operations` scope, must list `get_job` to poll it.
2372
+ - Another deploy or `drop_component` of the component on that node is refused with `409` until the rollout has finished.
2373
+ - A node running a version before v5.4.0 activates the release without a canary.
2317
2374
 
2318
- 4. **Deploy by reference (pinned commit)**: Use `by_ref=true` to send a pinned git reference instead of uploading a snapshot. The cluster fetches and builds from that exact commit SHA.
2375
+ 4. **Go back to a previous release by its `deployment_id`**: When a deploy replaces the live release, Harper keeps the replaced one under the `deployment_id` that deployed it. Activating that id puts back the exact installed release with no fetch, rebuild, or reinstall, where deploying an older commit installs it again from source. Find the id with `list_deployments`, then deploy it. On a cluster, restart with `rolling` and poll its job (step 3):
2319
2376
 
2320
2377
  ```bash
2321
- harper deploy by_ref=true restart=true replicated=true
2378
+ harper list_deployments project=my-app status=success
2379
+ harper deploy project=my-app deployment_id=<id> restart=rolling
2322
2380
  ```
2323
2381
 
2324
- - `by_ref` — Build the package reference from the local repository.
2325
- - `ref` _(optional)_ — Deploy a specific commit, tag, or branch instead of `HEAD`. Implies `by_ref`. Tags and branches in `refs/tags` and `refs/heads` namespaces are resolved to a full commit SHA before the deploy is sent.
2326
- - `credential` _(optional)_ — Set to `true` to authenticate the clone with the stored credential for the repository's host. Omit for public repositories.
2327
-
2328
- ```bash
2329
- # Deploy a specific tag
2330
- harper deploy ref=v1.2.0 restart=true replicated=true
2331
-
2332
- # Roll back by deploying an older commit
2333
- harper deploy ref=9f8c2a1 restart=true replicated=true
2382
+ ```json
2383
+ {
2384
+ "operation": "deploy_component",
2385
+ "project": "my-app",
2386
+ "deployment_id": "<id of the deployment you want back>",
2387
+ "restart": "rolling"
2388
+ }
2334
2389
  ```
2335
2390
 
2336
- **Important constraints on refs:**
2337
- - A full commit SHA is accepted directly with no resolution.
2338
- - Every other `ref` must resolve to something a clone can fetch: `refs/tags/*` or `refs/heads/*`, or a bare branch or tag name.
2339
- - Qualified refs outside those two namespaces (e.g., `refs/pull/123/head`) are rejected.
2340
- - Commit and push before deploying — the cluster clones from the remote and only sees pushed commits.
2341
- - Run `git fetch` if a ref can't be resolved, or pass a full commit SHA.
2391
+ - `list_deployments` returns records newest first. Filter by `project` and `status`; omit `limit` to return every matching record. A deploy in progress reads `pending`, or `loading`, until its `completed_at` is set.
2392
+ - The newest `success` is not necessarily the release every node is serving: a rolling deploy's record says `success` before the other nodes have taken the release, and keeps saying it if one rejects it.
2393
+ - Redeploy an older commit by `ref` (step 7) only when the release is no longer kept.
2394
+ - The activation restores the root config entry that deployment published. The release it replaces is kept in turn, so you can go forward again the same way.
2395
+ - Kept releases and staged builds count against `deployment.stagingRetention.maxCount` (default `5`). Each kept release is a full installed copy of the component, `node_modules` included, so budget disk for it. `0` keeps no replaced release.
2396
+ - A release made live before v5.3.0, or deployed from a `file:` directory, is not kept.
2397
+ - Each node answers for itself. A node that never had the release, or no longer keeps it, answers `404`, which `deploy_component` reports as a failed peer. `409` means the build is there but can't be activated.
2342
2398
 
2343
- 5. **Handle private repositories**: Pass `credential=true` so the CLI attaches a credentials reference that the cluster resolves in memory at clone time. No token travels in the operation body or lands on disk.
2399
+ 5. **Stage a release to make live later**: `activate=false` (`activate: false` in the operation) builds and installs the release on each node without making it live, so the slow, fallible part happens when you choose and the cut-over is quick. The result's `deployment_id` names the staged build:
2344
2400
 
2345
- ```bash
2346
- harper deploy by_ref=true credential=true restart=true replicated=true
2401
+ ```json
2402
+ {
2403
+ "operation": "deploy_component",
2404
+ "project": "my-app",
2405
+ "package": "npm:@my-org/my-app@1.2.3",
2406
+ "activate": false
2407
+ }
2347
2408
  ```
2348
2409
 
2349
- Provision the credential once with `setup=true` before using `credential=true`.
2410
+ ```json
2411
+ {
2412
+ "deployment_id": "a3f8c2d1-...",
2413
+ "message": "Staged: my-app. Deploy it with deploy_component deployment_id=a3f8c2d1-..."
2414
+ }
2415
+ ```
2350
2416
 
2351
- 6. **Provision a deploy credential**: Run `harper deploy setup=true` once per component and source to provision credentials for private deploys. This operation requires **super_user** — run it with an administrative credential, not the CI identity.
2417
+ Make it live by deploying that `deployment_id`, as in step 4. Nothing changes until then, and `get_deployment` reports it as `staged`.
2418
+ - Pass `restart` on the activation, not the stage: `restart` is rejected alongside `activate: false`.
2419
+ - The activation takes no `package`, `payload`, `credentials`, install options, or `urlPath`/`host`. They are rejected, because the staged build already decided them.
2420
+ - Activating the release that is already live succeeds without a swap, so if an activation fails partway (a peer was unreachable, or the root config entry wasn't written), retry the same `deployment_id`. A node that already switched answers success, one still holding the build switches now, and one that holds neither answers `404`; the retry also finishes what the failed attempt left half done. With `restart`, a node where the release is already live still restarts every worker, isolated applications' included. Without it, the node marks the component as needing a restart.
2421
+ - A staged build outlives later deploys: stage v2, deploy v3, and activating v2's id returns the component to v2.
2422
+ - `file:` directory sources cannot be staged.
2423
+ - Upgrade every node before staging. A node before v5.3.0 doesn't recognize `activate: false`, and serves the release immediately. A peer that doesn't confirm staging fails the operation with the node names.
2352
2424
 
2353
- ```bash
2354
- harper deploy setup=true
2355
- ```
2425
+ 6. **Choose a package source**: The `package` field accepts any valid npm dependency value:
2356
2426
 
2357
- This interactive command:
2358
- 1. Fetches the cluster's public key with `get_secrets_public_key`.
2359
- 2. Encrypts the token locally into an `enc:v1:` envelope.
2360
- 3. Stores only the ciphertext with `set_secret`, in the component-scoped tier.
2361
- 4. Grants the component permission to resolve it with `grant_secret`.
2362
- 5. Prints the `credentials` reference for the deploy to use.
2427
+ | Source | `package` value |
2428
+ | ----------------------- | --------------------------------------------------------------------------------------------- |
2429
+ | Current local directory | Omit `package` |
2430
+ | npm package | `package="@harperdb/status-check"` |
2431
+ | GitHub (public) | `package="HarperFast/status-check"` or `package="https://github.com/HarperFast/status-check"` |
2432
+ | Private repo (SSH) | `package="git+ssh://git@github.com:HarperDB/secret-app.git"` |
2433
+ | Tarball | `package="https://example.com/application.tar.gz"` |
2363
2434
 
2364
- **Use a fine-grained PAT** for GitHub repositories. The prompt defaults to a fine-grained personal access token with **Contents: Read-only** on that one repository. If you use the `gh` CLI session token instead, it typically carries `read:org`, `repo`, `gist`, and `workflow` scopes across your whole account — the CLI prints a warning if you choose it. Use the narrowest credential that does the job, because the stored token is replayed on every cold deploy and rollback.
2435
+ For git tags, use the `semver` directive: `HarperFast/application-template#semver:v1.0.0`. For an SSH-based private repo, register the key with `add_ssh_key` first.
2365
2436
 
2366
- 7. **Use environment variables for CI/CD**: Instead of `harper login`, export credentials as environment variables.
2437
+ 7. **Deploy by reference for a reproducible source**: `by_ref=true` sends a pinned git reference instead of uploading the working directory, and the cluster fetches and builds that exact commit. It resolves the repository's `origin` remote and the current commit, then deploys `package=git+https://github.com/<owner>/<repo>.git#<full commit SHA>`. Pass `ref` to deploy a specific tag or commit instead of `HEAD`; it implies `by_ref`.
2367
2438
 
2368
2439
  ```bash
2369
- export HARPER_CLI_USERNAME=<username>
2370
- export HARPER_CLI_PASSWORD=<password>
2371
- harper deploy \
2372
- project=<name> \
2373
- package=<package> \
2374
- target=<remote> \
2375
- restart=true \
2376
- replicated=true
2440
+ harper deploy by_ref=true restart=true replicated=true
2441
+ harper deploy ref=v1.2.0 restart=true replicated=true
2442
+ harper deploy ref=9f8c2a1 restart=true replicated=true
2377
2443
  ```
2378
2444
 
2379
- 8. **Use dedicated auth parameters for one-off commands** (not recommended for production): Pass `auth_username` and `auth_password` directly. These take precedence over environment variables and saved login tokens.
2445
+ - A `ref` is resolved to a full commit SHA before the deploy is sent, from the local checkout or the remote. A full commit SHA is accepted directly. If a `ref` can't be resolved, the deploy stops: run `git fetch` and retry, or pass a full SHA.
2446
+ - Any other `ref` must name something a clone can fetch: `refs/heads/*` or `refs/tags/*`, or a bare branch or tag name. A qualified ref outside those, such as `refs/pull/123/head`, is rejected up front.
2447
+ - Commit and push first: the cluster clones from the remote. `by_ref` warns when the working tree is dirty, and when the commit isn't on any remote branch. The second check is skipped under GitHub Actions.
2448
+ - On a GitHub Actions `pull_request` run, `by_ref` deploys the pull request's head commit. If the event payload isn't readable, the deploy stops; pass the commit explicitly.
2449
+ - The cluster installs and builds from source on each node, so commit your lockfile if the build must be reproducible. If the build can't run on the node, deploy the built output as a payload instead.
2450
+
2451
+ 8. **Deploy a private repository by reference**: Pass `credential=true` with `by_ref=true`. The CLI attaches a `credentials` reference, and the cluster resolves the secret in memory at clone time, so no token travels in the operation body or lands on disk.
2380
2452
 
2381
2453
  ```bash
2382
- harper deploy \
2383
- project=<name> \
2384
- package=<package> \
2385
- auth_username=<username> \
2386
- auth_password=<password> \
2387
- target=<remote> \
2388
- restart=true \
2389
- replicated=true
2454
+ harper deploy by_ref=true credential=true restart=true replicated=true
2390
2455
  ```
2391
2456
 
2457
+ Provision that credential once per component and source with `harper deploy setup=true`. (With `provider=github-actions`, it sets up GitHub Actions deploys instead: see [deploying-from-ci.md](deploying-from-ci.md).) It is interactive, and its operations require **super_user**, so run it with an administrative credential, not the CI identity it provisions for. It:
2458
+ 1. Fetches the cluster's public key with `get_secrets_public_key`.
2459
+ 2. Encrypts the token locally into an `enc:v1:` envelope, so the plaintext never leaves your machine.
2460
+ 3. Stores only the ciphertext with `set_secret`, scoped to the component, never in the global `processEnv` tier.
2461
+ 4. Grants the component permission to resolve it with `grant_secret`.
2462
+ 5. Prints the `credentials` reference for the deploy to use.
2463
+
2464
+ Use a **fine-grained** personal access token with **Contents: Read-only** on that one repository. Avoid the `gh` CLI's session token: it typically carries `repo`, `read:org`, `gist`, and `workflow` scopes across your whole account. The sealed token is durable, and later deploys reuse it.
2465
+
2392
2466
  #### Examples
2393
2467
 
2394
- **Standard deploy after login:**
2468
+ **Deploy after logging in, and wait for a rolling restart:**
2395
2469
 
2396
2470
  ```bash
2397
2471
  harper login https://my-cluster.harperdbcloud.com
@@ -2399,57 +2473,370 @@ harper deploy \
2399
2473
  project=my-app \
2400
2474
  package="HarperFast/my-app" \
2401
2475
  target=https://my-cluster.harperdbcloud.com \
2402
- restart=true \
2403
- replicated=true
2476
+ restart=rolling \
2477
+ json=true > deploy.json
2478
+ # Poll the node that received the deploy until the job is COMPLETE or ERROR
2479
+ harper get_job id=<restartJobId from deploy.json> json=true
2404
2480
  ```
2405
2481
 
2406
- **Deploy by reference with a tag:**
2482
+ **Roll back to the release a deploy replaced, node by node:**
2407
2483
 
2408
2484
  ```bash
2409
- harper deploy ref=v1.2.0 restart=true replicated=true
2485
+ harper list_deployments project=my-app status=success
2486
+ harper deploy project=my-app deployment_id=<previous deployment_id> restart=rolling json=true
2487
+ # Then poll its restartJobId with get_job, as above
2410
2488
  ```
2411
2489
 
2412
- **Deploy a private GitHub repo by reference:**
2490
+ **Stage a release now, and make it live in a maintenance window:**
2413
2491
 
2414
2492
  ```bash
2415
- # Provision credential once (requires super_user)
2493
+ harper deploy project=my-app package=npm:@my-org/my-app@1.2.3 activate=false
2494
+ # Later, with the deployment_id the stage returned
2495
+ harper deploy project=my-app deployment_id=<staged deployment_id> restart=rolling
2496
+ ```
2497
+
2498
+ **Deploy a pull request's head commit by reference in GitHub Actions:**
2499
+
2500
+ ```bash
2501
+ harper deploy ref=${{ github.event.pull_request.head.sha }} restart=true replicated=true
2502
+ ```
2503
+
2504
+ **Deploy a private repository by reference:**
2505
+
2506
+ ```bash
2507
+ # Once, as super_user
2416
2508
  harper deploy setup=true
2417
2509
 
2418
- # Deploy using stored credential
2510
+ # Every deploy
2419
2511
  harper deploy by_ref=true credential=true restart=true replicated=true
2420
2512
  ```
2421
2513
 
2422
- **GitHub Actions — pull request deploy:**
2514
+ ### 4.2 Deploying a Harper Application from GitHub Actions with OIDC
2515
+
2516
+ Instructions for the agent to configure and execute credential-free Harper deployments from GitHub Actions using OIDC trusted publishing, including one-command setup and complete workflow examples.
2517
+
2518
+ #### When to Use
2519
+
2520
+ Apply this rule when asked to deploy from CI, to set up or fix a deploy workflow (including the `.github/workflows/deploy.yaml` that create-harper projects ship), or to move a pipeline off a stored Harper credential such as `HARPER_CLI_REFRESH_TOKEN`. Use it when setting up OIDC trust for the first time, writing or debugging a deploy workflow, or revoking/rotating pipeline access.
2521
+
2522
+ #### How It Works
2523
+
2524
+ ##### One-Command Setup (Branch-Based Deploys)
2525
+
2526
+ 1. **Run setup once** from the project checkout, signed in as a `super_user`:
2527
+
2528
+ ```bash
2529
+ harper login https://my-cluster.example.com:9925
2530
+ harper deploy setup=true provider=github-actions project=my-app
2531
+ ```
2532
+
2533
+ If your workflow file is not `.github/workflows/deploy.yaml`, pass `workflow=` explicitly — otherwise the policy pins `deploy.yaml` and runs of your workflow cannot match it:
2534
+
2535
+ ```bash
2536
+ harper deploy setup=true provider=github-actions project=my-app workflow=.github/workflows/deploy.yml
2537
+ ```
2538
+
2539
+ 2. **Understand what setup does** — in order:
2540
+ - Resolves the GitHub repository's numeric id and canonical name from the `origin` remote (or `repo=<owner>/<name>`). Pass `repository_id=<id>` when the API is unavailable (e.g., private repo without `gh`).
2541
+ - **The workflow is checked before anything is written.** It must be able to run on `main` (`branch=`) and run a job in the `production` environment (`environment=`) with `permissions: id-token: write`; one that never could stops setup. What it can't decide is reported as not verified.
2542
+ - Creates a role and a user named `<project>-ci-deploy` with permissions limited to `deploy_component`, `get_job`, and `registration_info`, and a trust policy named `github-actions-<project>`.
2543
+ - Reads the policy back and compares it claim by claim with what it intended to write.
2544
+ - Sets the repository's `HARPER_CLI_TARGET` Actions variable via `gh` if one does not already exist.
2545
+
2546
+ 3. **Handle idempotency**: setup only creates — it never updates. A role, user, or policy that already exists but differs stops setup and names what differs. To replace a policy:
2547
+
2548
+ ```bash
2549
+ harper drop_oidc_trust id=github-actions-<project>
2550
+ harper deploy setup=true provider=github-actions project=my-app
2551
+ ```
2552
+
2553
+ ##### Manual Setup (Tag-Based or Custom Deploys)
2554
+
2555
+ Use this path when deploying on version tags, because a branch-pinned policy cannot match a tag ref.
2556
+
2557
+ 1. **Create a deploy-only role and user**:
2558
+
2559
+ ```bash
2560
+ harper add_role role=ci_deploy permission='{"operations":["deploy_component","get_job","get_deployment","registration_info"]}'
2561
+ harper add_user username=ci-deploy role=ci_deploy active=true password="$(openssl rand -base64 32)"
2562
+ ```
2563
+
2564
+ - `deploy_component` — the deploy itself.
2565
+ - `get_job` — required for the wait step; an `operations` allowlist refuses anything not listed, so without it the wait step fails with `403`.
2566
+ - `get_deployment` — reads the deployment record.
2567
+ - `registration_info` — version check `harper deploy` makes before uploading.
2568
+ - `restart_service` is not needed: Harper starts the rolling restart's job itself. The password is required to create the user but a GitHub Actions pipeline never uses it, so this command discards it.
2569
+
2570
+ 2. **Add a trust policy** pinned to `repository_id`, `workflow_path`, and `environment` (use this shape when the tag is unknown at policy-write time):
2571
+
2572
+ ```bash
2573
+ harper add_oidc_trust \
2574
+ id=my-app-release \
2575
+ issuer=https://token.actions.githubusercontent.com \
2576
+ audience=https://my-cluster.example.com:9925/ \
2577
+ user=ci-deploy \
2578
+ claims='{"repository_id":"67890","workflow_path":"my-org/my-app/.github/workflows/deploy.yml","environment":"production"}'
2579
+ ```
2580
+
2581
+ Key claim rules:
2582
+ - `repository_id` is immutable — survives renames. Look it up with `gh api repos/my-org/my-app --jq .id`.
2583
+ - `workflow_path` pins the workflow file. Harper derives it from the token's `workflow_ref` by removing the `@<ref>` suffix, so it is `<owner>/<repo>/<path>`.
2584
+ - `environment` is the ref gate. Without it, anyone who can push a branch can mint a token.
2585
+ - `audience` must be the exact target URL with its port and a **trailing slash**.
2586
+ - A workflow that deploys on a push to `main` can pin `workflow_ref` (`my-org/my-app/.github/workflows/deploy.yml@refs/heads/main`) instead, which gates the ref by itself.
2587
+
2588
+ 3. **Configure the GitHub environment**: in **Settings → Environments → production**, set **Deployment branches and tags** to the `v*` tag pattern. Add required reviewers if a release needs approval.
2589
+
2590
+ 4. **Store the target as a repository variable** (not a secret — it is not sensitive):
2591
+
2592
+ ```bash
2593
+ gh variable set HARPER_CLI_TARGET --repo my-org/my-app --body https://my-cluster.example.com:9925/
2594
+ ```
2595
+
2596
+ ##### How OIDC Authentication Works in the Workflow
2597
+
2598
+ Once a trust policy exists, every `harper` command in the deploy job authenticates through it automatically when all four conditions hold:
2599
+
2600
+ 1. **The job may request an identity token.** On GitHub Actions that is `permissions: id-token: write` on the job.
2601
+ 2. **`HARPER_CLI_TARGET` is the policy's `audience`.** The CLI requests its token for the target it connects to, so the target must be the URL the policy names, with its port and trailing slash. Use `vars.HARPER_CLI_TARGET` — it is not sensitive, so a repository variable rather than a secret.
2602
+ 3. **The run matches the policy's claims**: its repository, its workflow file and branch, and its environment.
2603
+ 4. **Nothing else is configured to authenticate.** `auth_username=`/`auth_password=`, credentials in the target URL, `HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`, `HARPER_CLI_REFRESH_TOKEN` or `HARPER_CLI_OPERATION_TOKEN`, and a saved `harper login` all outrank the exchange.
2604
+
2605
+ What each command may then do is up to the role of the user the policy names: a role that lists only `deploy_component`, `get_job` and `registration_info`, like the one setup creates, refuses anything else with a `403`.
2606
+
2607
+ The exchange runs only when GitHub sets `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, which it does for a job that declares `permissions: id-token: write`. Without them, the CLI falls through to its other credential sources rather than reporting a failure.
2608
+
2609
+ Nothing in the workflow names the policy. The CLI prints to stderr which policy authenticated it and as whom:
2610
+
2611
+ ```
2612
+ Requesting a GitHub Actions identity token for https://my-instance.harperdb.io:9925/...
2613
+ Authenticated as 'ci-deploy' via OIDC trust policy 'my-app-prod'.
2614
+ ```
2615
+
2616
+ Diagnose a rejected exchange with `list_oidc_trust` (check for `invalid_reason` on each entry) and the instance's `oidc-trust` log. The server deliberately does not report which check failed.
2617
+
2618
+ ##### Policy Specificity for GitHub Actions
2619
+
2620
+ For `https://token.actions.githubusercontent.com`, a policy must satisfy all three of these:
2621
+
2622
+ | Requirement | Satisfied by one of | Left open otherwise |
2623
+ | ---------------------- | ------------------------------------------------------------------------ | ----------------------------------------------- |
2624
+ | **Pin the repository** | `repository_id`, `repository` | Any repository |
2625
+ | **Pin the workflow** | `workflow_ref`, `workflow_path`, `job_workflow_ref`, `job_workflow_path` | Any workflow in that repository |
2626
+ | **Gate the ref** | `workflow_ref`, `ref`, `environment` | Any branch that can be pushed to the repository |
2627
+
2628
+ Additional constraints:
2629
+
2630
+ - `repository_id` is preferred over `repository` — it is immutable and survives renames.
2631
+ - A tag-triggered release cannot pin `workflow_ref` since the tag is unknown when the policy is written. Pin `workflow_path` and gate on `environment`.
2632
+ - `ref_type: tag` is not accepted as a ref gate, `sub` is not accepted as a pin, and `job_workflow_ref` pins the workflow without gating the ref. `pull_request_target` runs are denied unless the policy constrains `event_name`.
2633
+
2634
+ ##### Revoking Access
2635
+
2636
+ - **Stop future runs**: `harper drop_oidc_trust id=github-actions-<project>` (does not revoke already-issued tokens, which are valid up to one hour).
2637
+ - **Stop an already-issued token**: `harper alter_user username=<project>-ci-deploy active=false`.
2638
+ - **No CI secret to rotate** — there is none.
2639
+
2640
+ #### Examples
2641
+
2642
+ ##### Path A: Deploy a Tagged Release (`v1.2.3`) from the Repository
2643
+
2644
+ Push a semver tag (`v1.2.3`), and every node clones and installs that commit. `by_ref=true` deploys `git+https://github.com/<owner>/<repo>.git#<sha>`, where the SHA is the commit the tag points to (`GITHUB_SHA`), and every node clones that exact commit even if the tag moves later. For a private repository, add `credential=true` to attach the stored credential (`deploy.my-app.git.github.com`). If the repository needs a build step to be runnable, don't reach for `install_allow_scripts`: use Path B.
2645
+
2646
+ ```yaml
2647
+ # .github/workflows/deploy.yml
2648
+ name: Deploy to Harper
2649
+ on:
2650
+ push:
2651
+ tags: ['v*']
2652
+
2653
+ jobs:
2654
+ deploy:
2655
+ runs-on: ubuntu-latest
2656
+ environment: production
2657
+ permissions:
2658
+ contents: read
2659
+ id-token: write
2660
+ env:
2661
+ HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }}
2662
+ steps:
2663
+ - uses: actions/checkout@v4
2664
+ with:
2665
+ persist-credentials: false
2666
+ - uses: actions/setup-node@v4
2667
+ with:
2668
+ node-version: 22
2669
+ - run: npm install --global harper@^5.3
2670
+ - name: Deploy the tagged commit
2671
+ run: harper deploy project=my-app by_ref=true restart=rolling json=true > deploy.json
2672
+ - name: Wait for every node to take the release
2673
+ run: |
2674
+ JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
2675
+ [ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
2676
+ LAST="could not be read"
2677
+ for attempt in $(seq 60); do
2678
+ sleep 10
2679
+ harper get_job id="$JOB_ID" json=true > job.json || continue
2680
+ case $(jq -r '.[0].status // "MISSING"' job.json) in
2681
+ COMPLETE) exit 0 ;;
2682
+ ERROR) jq -r '.[0].message' job.json; exit 1 ;;
2683
+ MISSING) LAST="was not on the node that answered; its outcome is unknown" ;;
2684
+ *) LAST="was still running" ;;
2685
+ esac
2686
+ done
2687
+ echo "After 60 polls, job $JOB_ID $LAST"
2688
+ exit 1
2689
+ ```
2690
+
2691
+ Key details:
2692
+
2693
+ - **No secrets.** `HARPER_CLI_TARGET` is the only thing the workflow is given. `id-token: write` lets the CLI request an identity token, and `environment: production` puts the `environment` claim the trust policy requires into it.
2694
+ - **`persist-credentials: false`** keeps the job's `GITHUB_TOKEN` out of `.git/config`.
2695
+ - **`harper@^5.3`** pins the CLI's major version. 5.3 is the first CLI that authenticates with the identity token.
2696
+ - **`replicated` isn't needed.** On Harper Fabric and Harper Pro, a deploy goes to every node unless you pass `replicated=false`. Each node fetches and installs the package itself.
2697
+ - **`restart=rolling`** restarts nodes one at a time. Use `restart=true` for a single-node or dev instance.
2698
+ - **`json=true`** prints the deploy's result as JSON on stdout; progress and authentication messages go to stderr.
2699
+ - **The wait step** polls `restartJobId` via `get_job` until `COMPLETE` or `ERROR`, checking `.[0].status`. It retries a poll that fails (such as one that reaches a node while it restarts) and gives up after 60 polls (~10 minutes).
2700
+ - **`certification`**: on v5.4.0 and later, `restart=rolling` loads the release in a canary worker on each node before that node serves it, so a release that fails to load is rejected. The `certification` field in the deploy result reflects this. With `restart=rolling` on v5.4.0 and later, the deploy can report `success` before the rolling job has visited all nodes — wait on the job, not the record.
2701
+ - **The job lives on one node.** Through a **load-balanced** cluster URL, polls must eventually reach that node. Routing that always sends polls to a different node than the deploy causes the wait step to fail saying the job was not on the node that answered.
2702
+
2703
+ ##### Path B: Build in CI, Publish to GitHub Packages, Deploy the Artifact
2704
+
2705
+ Give `package.json` a scoped name and `"publishConfig": { "registry": "https://npm.pkg.github.com" }`:
2706
+
2707
+ ```json
2708
+ {
2709
+ "name": "@my-org/my-app",
2710
+ "version": "1.2.3",
2711
+ "publishConfig": { "registry": "https://npm.pkg.github.com" },
2712
+ "files": ["config.yaml", "schema.graphql", "dist/"]
2713
+ }
2714
+ ```
2715
+
2716
+ The deploy installs the version the tag names, and `npm publish` publishes the version in `package.json`, so they must agree: `npm version 1.2.3` sets one and creates the other as `v1.2.3`.
2717
+
2718
+ ```yaml
2719
+ # .github/workflows/deploy.yml
2720
+ name: Build, publish, and deploy to Harper
2721
+ on:
2722
+ push:
2723
+ tags: ['v*']
2724
+
2725
+ jobs:
2726
+ publish:
2727
+ runs-on: ubuntu-latest
2728
+ permissions:
2729
+ contents: read
2730
+ packages: write
2731
+ steps:
2732
+ - uses: actions/checkout@v4
2733
+ with:
2734
+ persist-credentials: false
2735
+ - uses: actions/setup-node@v4
2736
+ with:
2737
+ node-version: 22
2738
+ registry-url: https://npm.pkg.github.com
2739
+ scope: '@my-org'
2740
+ - run: npm ci
2741
+ env:
2742
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2743
+ - run: npm run build
2744
+ - run: npm test
2745
+ - name: Publish to GitHub Packages
2746
+ run: npm publish
2747
+ env:
2748
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2749
+
2750
+ deploy:
2751
+ needs: publish
2752
+ runs-on: ubuntu-latest
2753
+ environment: production
2754
+ permissions:
2755
+ id-token: write
2756
+ env:
2757
+ HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }}
2758
+ steps:
2759
+ - uses: actions/setup-node@v4
2760
+ with:
2761
+ node-version: 22
2762
+ - run: npm install --global harper@^5.3
2763
+ - name: Deploy the published version
2764
+ run: |
2765
+ harper deploy project=my-app package="@my-org/my-app@${GITHUB_REF_NAME#v}" \
2766
+ credentials='[{"registry":"https://npm.pkg.github.com","scope":"@my-org","secret":"deploy.my-app.npm.pkg.github.com"}]' \
2767
+ restart=rolling json=true > deploy.json
2768
+ - name: Wait for every node to take the release
2769
+ run: |
2770
+ JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
2771
+ [ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
2772
+ LAST="could not be read"
2773
+ for attempt in $(seq 60); do
2774
+ sleep 10
2775
+ harper get_job id="$JOB_ID" json=true > job.json || continue
2776
+ case $(jq -r '.[0].status // "MISSING"' job.json) in
2777
+ COMPLETE) exit 0 ;;
2778
+ ERROR) jq -r '.[0].message' job.json; exit 1 ;;
2779
+ MISSING) LAST="was not on the node that answered; its outcome is unknown" ;;
2780
+ *) LAST="was still running" ;;
2781
+ esac
2782
+ done
2783
+ echo "After 60 polls, job $JOB_ID $LAST"
2784
+ exit 1
2785
+ ```
2786
+
2787
+ Two jobs keep the identity token isolated: `publish` installs, builds, tests and publishes with `contents: read` and `packages: write` only — it cannot request an identity token. `deploy` runs once `publish` finishes (`needs: publish`), holds `environment: production` and `id-token: write`, and installs only the Harper CLI before deploying.
2788
+
2789
+ Credential roles:
2790
+
2791
+ - **Publishing** and installing private `@my-org` dependencies use the workflow's ephemeral `GITHUB_TOKEN` (`packages: write` includes read). Both happen entirely inside `publish`.
2792
+ - **Installing on Harper nodes** uses the durable `read:packages` token stored as `deploy.my-app.npm.pkg.github.com` — because installs can happen long after this workflow ends.
2793
+
2794
+ For `my-app`, the credential names are `deploy.my-app.git.github.com` for the repository and `deploy.my-app.npm.pkg.github.com` for GitHub Packages. Store a durable credential once from your machine:
2423
2795
 
2424
2796
  ```bash
2425
- harper deploy ref=${{ github.event.pull_request.head.sha }} restart=true replicated=true
2797
+ harper deploy setup=true provider=github project=my-app
2798
+ # or for GitHub Packages:
2799
+ harper deploy setup=true provider=npm project=my-app registry=https://npm.pkg.github.com scope=@my-org
2426
2800
  ```
2427
2801
 
2428
- **CI/CD deploy using environment variables:**
2802
+ For a private repository, use a GitHub fine-grained personal access token with **Contents: read-only** scoped to the application repository. For GitHub Packages, use a token with the `read:packages` scope. Do not use the workflow's ephemeral `GITHUB_TOKEN` for the durable credential — it expires when the job ends.
2803
+
2804
+ ##### Check the Deployment Record
2429
2805
 
2430
2806
  ```bash
2431
- export HARPER_CLI_USERNAME=<username>
2432
- export HARPER_CLI_PASSWORD=<password>
2433
- harper deploy \
2434
- project=my-app \
2435
- package="@myorg/my-app" \
2436
- target=https://my-cluster.harperdbcloud.com \
2437
- restart=true \
2438
- replicated=true
2807
+ harper get_deployment deployment_id="$(jq -r .deployment_id deploy.json)" json=true
2808
+ ```
2809
+
2810
+ Only the node you called writes that record, so with `restart=rolling` on v5.4.0 and later it can report `success` before the job has visited the other nodes, and keeps reporting it if one of them rejects the release. Wait on the job, not the record, to confirm all nodes took the release. `list_deployments` gives the history of what was deployed and when.
2811
+
2812
+ ##### List and Inspect Trust Policies
2813
+
2814
+ ```bash
2815
+ harper list_oidc_trust
2816
+ ```
2817
+
2818
+ Returns `{ "policies": [ ... ] }`. Each entry includes `id`, `issuer`, `audience`, `claims`, `user`, `enabled`, and optionally `invalid_reason`. Check `invalid_reason` first when diagnosing a rejected exchange — it is set when the policy is malformed or its user was deleted or deactivated.
2819
+
2820
+ ##### Drop a Trust Policy
2821
+
2822
+ ```bash
2823
+ harper drop_oidc_trust id=github-actions-<project>
2439
2824
  ```
2440
2825
 
2441
2826
  #### Notes
2442
2827
 
2443
- - The cluster's Application URL is found on the **Config → Overview** page of the Fabric dashboard.
2444
- - `harper deploy setup=true` calls `get_secrets_public_key`, `set_secret`, and `grant_secret`, all of which require **super_user**. Do not run it with the CI identity.
2445
- - The `enc:v1:` envelope means the plaintext token never leaves your machine — only ciphertext is stored and replicated.
2446
- - Secrets are stored scoped to the component, not in the global `processEnv` tier. If a global secret already exists at the derived name, it is converted to the scoped tier.
2447
- - For SSH-based private repos, use the `add_ssh_key` operation to register keys before deploying.
2448
- - If your application requires a build step that cannot run on the cluster node, deploy a built payload (omit `by_ref`) instead of deploying by reference.
2449
- - The unpushed-commit check is skipped under GitHub Actions; the dirty-tree warning still applies.
2450
- - Annotated tags in `refs/tags` resolve to the commit they point at, not the tag object.
2828
+ - `HARPER_CLI_REFRESH_TOKEN` and any other stored credential outrank the OIDC exchange. Remove them from the workflow if switching to OIDC.
2829
+ - **Go back to a previous release** by activating its `deployment_id`, with no rebuild or reinstall: `harper deploy project=my-app deployment_id=<id> restart=rolling`. To deploy a release that is no longer kept, re-run the deploy with its tag or version.
2830
+ - **Rotating the deploy credential**: run `harper deploy setup=true` again with the same name and a new value. The pipeline doesn't change.
2831
+ - **Revoking the pipeline's access**: `harper drop_oidc_trust` stops new runs; `harper alter_user` also stops a token already issued (valid up to one hour).
2832
+ - The `<project>-ci-deploy` user created by setup can deploy any component on the cluster, not only the named project. Access control comes down to who can merge to the branch.
2833
+ - The `audience` in `add_oidc_trust` must be the exact target URL with its port and a trailing slash.
2834
+ - For tag-triggered workflows, `provider=github-actions` setup is not sufficient — it pins a branch. Write the policy manually using `add_oidc_trust` with `workflow_path` and `environment` claims.
2835
+ - `token.actions.githubusercontent.com` is the required `issuer` value for GitHub Actions OIDC policies.
2836
+ - Git-host credentials require git ≥ 2.31 on the Harper nodes and are not supported on Windows nodes. Registry credentials have no such constraints.
2837
+ - See [deploying-to-harper-fabric.md](deploying-to-harper-fabric.md) for cluster-level deployment context.
2451
2838
 
2452
- ### 4.2 Creating a Harper Fabric Account and Cluster
2839
+ ### 4.3 Creating a Harper Fabric Account and Cluster
2453
2840
 
2454
2841
  Follow these steps to set up your Harper Fabric environment for deployment.
2455
2842
 
@@ -2460,7 +2847,7 @@ Follow these steps to set up your Harper Fabric environment for deployment.
2460
2847
  3. **Create a Cluster**: Create a new cluster. This can be on the free tier, no credit card required.
2461
2848
  4. **Set Credentials**: During setup, set the cluster username and password to finish configuring it.
2462
2849
  5. **Get Application URL**: Navigate to the **Config** tab and copy the **Application URL**.
2463
- 6. **Configure Environment**: Update your `.env` file or GitHub Actions secrets with cluster-specific credentials.
2850
+ 6. **Connect the CLI**: Run `harper login` with the Application URL to store a token for the cluster, and set the URL as `HARPER_CLI_TARGET` in your `.env` file. Prefer this to putting the cluster username and password in `.env`, and don't store them in GitHub Actions secrets: to deploy from GitHub Actions, set up OIDC trusted publishing instead (see [deploying-from-ci](deploying-from-ci.md)).
2464
2851
  7. **Next Steps**: See the [deploying-to-harper-fabric](deploying-to-harper-fabric.md) rule for detailed instructions on deploying your application successfully.
2465
2852
 
2466
2853
  #### Examples
@@ -2468,12 +2855,15 @@ Follow these steps to set up your Harper Fabric environment for deployment.
2468
2855
  ##### Environment Configuration
2469
2856
 
2470
2857
  ```bash
2471
- CLI_TARGET_USERNAME='YOUR_CLUSTER_USERNAME'
2472
- CLI_TARGET_PASSWORD='YOUR_CLUSTER_PASSWORD'
2473
- CLI_TARGET='YOUR_CLUSTER_URL'
2858
+ harper login YOUR_CLUSTER_URL
2859
+ ```
2860
+
2861
+ ```bash
2862
+ # .env
2863
+ HARPER_CLI_TARGET='YOUR_CLUSTER_URL'
2474
2864
  ```
2475
2865
 
2476
- ### 4.3 Creating Harper Applications
2866
+ ### 4.4 Creating Harper Applications
2477
2867
 
2478
2868
  The fastest way to start a new Harper project is using the `create-harper` CLI tool. This command
2479
2869
  initializes a project with a standard folder structure, essential configuration files, and basic
@@ -2516,12 +2906,79 @@ npm create harper@latest my-app --template default
2516
2906
 
2517
2907
  #### Next Steps
2518
2908
 
2519
- 1. **Configure Environment**: Set up your `.env` file with local or cloud credentials.
2520
- 2. **Define Schema**: Modify `schema.graphql` to fit your application's data model.
2521
- 3. **Start Development**: Run `npm run dev` to start the local Harper instance.
2522
- 4. **Deploy**: Use `npm run deploy` to push your application to Harper Fabric.
2909
+ 1. **Define Schema**: Modify `schema.graphql` to fit your application's data model.
2910
+ 2. **Start Development**: Run `npm run dev` to start the local Harper instance.
2911
+ 3. **Connect the CLI to your cluster**: Create a cluster (see
2912
+ [creating-a-fabric-account-and-cluster](creating-a-fabric-account-and-cluster.md)), run `harper login` with its
2913
+ Application URL as a super user, and set that URL as `HARPER_CLI_TARGET` in the project's `.env` file. Don't put a
2914
+ cluster username or password in `.env` or in GitHub secrets.
2915
+ 4. **Deploy**: See [Deploying](#deploying).
2916
+
2917
+ #### Deploying
2918
+
2919
+ Every template except the legacy `early-hints` example ships a GitHub Actions workflow,
2920
+ `.github/workflows/deploy.yaml`. It tests every pull request and deploys to the cluster when a change merges to `main`.
2921
+ The deploy job holds no Harper credential: its `harper` commands authenticate with GitHub's OIDC identity token under a
2922
+ trust policy on the cluster. The Next.js templates' workflow also runs `next build` before it uploads.
2923
+
2924
+ 1. **Set up CI deploys once**: Push the project to a GitHub repository, then run `npm run deploy:setup-ci` from the
2925
+ project, logged in to the cluster as a super user. It runs `harper deploy setup=true provider=github-actions` for the
2926
+ project, which creates a deploy-only role and user and a trust policy matched to the workflow, and sets the
2927
+ repository's `HARPER_CLI_TARGET` variable when the `gh` CLI is signed in (otherwise it prints the command to run).
2928
+ Running it again changes nothing. See [deploying-from-ci](deploying-from-ci.md) for what it checks and how to
2929
+ customize it.
2930
+
2931
+ The command needs the Harper 5.4.0 CLI or later (`harper version`); an earlier CLI can't run it. Without 5.4.0, set
2932
+ up the same role, user, policy and variable by hand, as a super user. The policy must match the scaffolded workflow:
2933
+ its `deploy.yaml` file on `main`, in the `production` environment. The `audience` is the cluster URL with its port
2934
+ and a trailing slash, and `HARPER_CLI_TARGET` must be that same string:
2523
2935
 
2524
- ### 4.4 Serving Web Content
2936
+ ```bash
2937
+ gh api repos/<owner>/<repo> --jq .id # the repository_id
2938
+ harper add_role role=<name>-ci-deploy permission='{"operations":["deploy_component","get_job","registration_info"]}'
2939
+ harper add_user username=<name>-ci-deploy role=<name>-ci-deploy active=true password="$(openssl rand -base64 32)"
2940
+ harper add_oidc_trust \
2941
+ id=github-actions-<name> \
2942
+ issuer=https://token.actions.githubusercontent.com \
2943
+ audience=https://my-cluster.example.com:9925/ \
2944
+ user=<name>-ci-deploy \
2945
+ claims='{"repository_id":"<id>","workflow_ref":"<owner>/<repo>/.github/workflows/deploy.yaml@refs/heads/main","environment":"production"}'
2946
+ gh variable set HARPER_CLI_TARGET --repo <owner>/<repo> --body https://my-cluster.example.com:9925/
2947
+ ```
2948
+
2949
+ If you restrict the `production` environment's deployment branches, allow `main`.
2950
+
2951
+ 2. **Deploy by merging to `main`**, or by running the workflow from the **Actions** tab. The workflow deploys with
2952
+ `restart=rolling` and waits for every node to take the release. The run's summary names the `deployment_id` and its
2953
+ `certification`.
2954
+ 3. **Deploy by hand with `npm run deploy`**: It runs `harper deploy project=<name> restart=rolling` from your machine, as
2955
+ the user you logged in with.
2956
+ 4. **Go back to an earlier release** by activating its `deployment_id`, with nothing reinstalled:
2957
+
2958
+ ```bash
2959
+ harper list_deployments project=<name>
2960
+ harper deploy project=<name> deployment_id=<id> restart=rolling
2961
+ ```
2962
+
2963
+ See [deploying-to-harper-fabric](deploying-to-harper-fabric.md).
2964
+
2965
+ Use your package manager's equivalent of `npm run` (`pnpm run`, `bun run`, `yarn`) if the project uses another one.
2966
+
2967
+ Projects created before create-harper 1.13.0 deploy on version tags with a `HARPER_CLI_REFRESH_TOKEN` secret, and
2968
+ don't have `deploy:setup-ci`. To move one over:
2969
+
2970
+ 1. Scaffold a new project with the same name and template, and copy its `.github/workflows/deploy.yaml` over the old
2971
+ workflow.
2972
+ 2. Set up the cluster for it, with the `project=` value that workflow passes to `harper deploy` as `<name>`: with the
2973
+ 5.4.0 CLI, `harper deploy setup=true provider=github-actions project=<name>`; with an earlier one, by hand as in
2974
+ step 1.
2975
+ 3. Push the new workflow to `main`.
2976
+ 4. Once the workflow's run on `main` has succeeded, wait step included, delete the `HARPER_CLI_REFRESH_TOKEN` secret.
2977
+ Deleting it doesn't revoke the token, which stays valid until it expires. To cut it off now, mint a new one for that
2978
+ user with `harper login --for-ci`, which invalidates the old one, or deactivate the user with `alter_user` if
2979
+ nothing else uses it.
2980
+
2981
+ ### 4.5 Serving Web Content
2525
2982
 
2526
2983
  Instructions for the agent to follow when serving web content from Harper.
2527
2984
 
@@ -2628,7 +3085,7 @@ Because `@harperfast/vite` builds on the node and `static` serves the output, de
2628
3085
 
2629
3086
  On deploy the plugin runs `vite build` at startup (and rebuilds when `files` change) while `static` serves the result. If you prefer to build in CI, commit the build output, point `static` at it, and omit `files` so the plugin stays idle while `static` serves the prebuilt assets. Either way, `npm create harper@latest` scaffolds a working setup for you.
2630
3087
 
2631
- ### 4.5 Harper Logging
3088
+ ### 4.6 Harper Logging
2632
3089
 
2633
3090
  Instructions for the agent to follow when implementing logging in Harper applications, including direct logger usage, tagged loggers, and console capture behavior.
2634
3091
 
@@ -2784,61 +3241,31 @@ Tagged entries appear in `hdb.log` with the tag in the header:
2784
3241
  - When logging to standard streams, run Harper in the foreground (`harper`, not `harper start`).
2785
3242
  - `TaggedLogger` is bound to the configured log level at creation time — always use `?.` on its methods.
2786
3243
 
2787
- ### 4.6 Load Environment Variables with loadEnv
3244
+ ### 4.7 Load Environment Variables with loadEnv
2788
3245
 
2789
3246
  Instructions for the agent to follow when loading environment variables from `.env` files into a Harper application using the `loadEnv` plugin.
2790
3247
 
2791
3248
  #### When to Use
2792
3249
 
2793
- Apply this rule when a Harper application needs to supply secrets, API endpoints, or other configuration values to component code via `process.env` without hardcoding them. Use `loadEnv` any time you need to load one or more `.env` files at application startup.
3250
+ Apply this rule when a Harper application needs to supply secrets, API endpoints, or other configuration values to component code via `process.env` without hardcoding them. Use this whenever a `.env` file must be loaded at application startup.
2794
3251
 
2795
3252
  #### How It Works
2796
3253
 
2797
- 1. **Declare `loadEnv` in `config.yaml`**: Add `loadEnv` as the first entry in `config.yaml`. It is built into Harper and requires no installation.
2798
-
2799
- ```yaml
2800
- loadEnv:
2801
- files: '.env'
2802
- ```
2803
-
2804
- 2. **Place `loadEnv` first**: Harper is a single-process application. List `loadEnv` before all other components so that environment variables are available on `process.env` before dependent components start.
3254
+ 1. **Declare `loadEnv` first in `config.yaml`**: Place `loadEnv` before all other components so environment variables are available when dependent components start.
2805
3255
 
2806
3256
  ```yaml
2807
3257
  # config.yaml — loadEnv must come first
2808
3258
  loadEnv:
2809
3259
  files: '.env'
2810
-
2811
- rest: true
2812
-
2813
- myApp:
2814
- files: './src/*.js'
2815
3260
  ```
2816
3261
 
2817
- 3. **Access loaded values in component code**: After `loadEnv` runs, all loaded values are available on `process.env` and shared across all components.
3262
+ 2. **Set the `files` option**: Provide one or more paths or glob patterns pointing to the env file(s) to load. This option is required.
2818
3263
 
2819
- 4. **Control override behavior**: By default, existing environment variables take precedence over values in `.env` files. Set `override: true` to make loaded values win instead.
3264
+ 3. **Access variables in component code**: After loading, all values from the specified files are available on `process.env` and shared across all components in the single Harper process.
2820
3265
 
2821
- ```yaml
2822
- loadEnv:
2823
- files: '.env'
2824
- override: true
2825
- ```
3266
+ 4. **Control override behavior with `override`**: By default, existing environment variables take precedence over values in `.env` files. Set `override: true` to reverse this — loaded values will overwrite variables already present in the shell or container environment.
2826
3267
 
2827
- 5. **Load multiple files**: Pass an array of paths or a glob pattern to `files`. Files are loaded in the order specified.
2828
-
2829
- ```yaml
2830
- loadEnv:
2831
- files:
2832
- - '.env'
2833
- - '.env.local'
2834
- ```
2835
-
2836
- or with a glob:
2837
-
2838
- ```yaml
2839
- loadEnv:
2840
- files: 'env-vars/*'
2841
- ```
3268
+ 5. **Load multiple files when needed**: Specify a list or a glob pattern under `files`. Files are loaded in the order specified.
2842
3269
 
2843
3270
  ##### Configuration Options
2844
3271
 
@@ -2849,10 +3276,17 @@ Apply this rule when a Harper application needs to supply secrets, API endpoints
2849
3276
 
2850
3277
  #### Examples
2851
3278
 
2852
- **Single file, default behavior:**
3279
+ **Basic usage — load a single `.env` file:**
2853
3280
 
2854
3281
  ```yaml
2855
- # config.yaml
3282
+ loadEnv:
3283
+ files: '.env'
3284
+ ```
3285
+
3286
+ **Full `config.yaml` with `loadEnv` declared first:**
3287
+
3288
+ ```yaml
3289
+ # config.yaml — loadEnv must come first
2856
3290
  loadEnv:
2857
3291
  files: '.env'
2858
3292
 
@@ -2862,29 +3296,40 @@ myApp:
2862
3296
  files: './src/*.js'
2863
3297
  ```
2864
3298
 
2865
- **Multiple files with override:**
3299
+ **Override existing environment variables:**
3300
+
3301
+ ```yaml
3302
+ loadEnv:
3303
+ files: '.env'
3304
+ override: true
3305
+ ```
3306
+
3307
+ **Load multiple files as a list:**
2866
3308
 
2867
3309
  ```yaml
2868
- # config.yaml
2869
3310
  loadEnv:
2870
3311
  files:
2871
3312
  - '.env'
2872
3313
  - '.env.local'
2873
- override: true
3314
+ ```
2874
3315
 
2875
- rest: true
3316
+ **Load multiple files using a glob pattern:**
2876
3317
 
2877
- myApp:
2878
- files: './src/*.js'
3318
+ ```yaml
3319
+ loadEnv:
3320
+ files: 'env-vars/*'
2879
3321
  ```
2880
3322
 
2881
3323
  #### Notes
2882
3324
 
2883
- - `loadEnv` loads values into `process.env` for **application** code only — it does not configure Harper itself.
2884
- - Harper's own instance-wide configuration is composed at startup **before** any component's `loadEnv` runs. Variables such as `HARPER_CONFIG`, `HARPER_SET_CONFIG`, and `HARPER_DEFAULT_CONFIG` delivered through a `.env` file are read too late and are ignored. Set Harper configuration directly in the configuration file or export variables in the real process/container environment before Harper starts.
2885
- - For production credentials, prefer the encrypted secrets store over a committed `.env` file. Secrets are also delivered to components via `process.env`.
3325
+ - `loadEnv` is built into Harper — it does not need to be installed, only declared in `config.yaml`.
3326
+ - Variables are loaded onto `process.env` and shared across all components. Declare `loadEnv` before any component that depends on those variables.
3327
+ - `loadEnv` supplies **application** environment variables only. It does **not** configure Harper itself. Harper's own configuration is composed at startup before any `loadEnv` runs, so variables such as `HARPER_CONFIG`, `HARPER_SET_CONFIG`, and `HARPER_DEFAULT_CONFIG` delivered through a `.env` file are read too late and are ignored.
3328
+ - To change Harper's own configuration, use the configuration file directly or export variables in the real process/container environment before Harper starts.
3329
+ - For production credentials, prefer the encrypted secrets store over a committed `.env` file. Secrets are delivered to components via `process.env` and never appear in operation logs or replication payloads.
3330
+ - Individual `.env` values can also be stored encrypted so plaintext never reaches the API, logs, or replication payload.
2886
3331
 
2887
- ### 4.7 v5 Upgrade: Breaking Changes and Migration Guide
3332
+ ### 4.8 v5 Upgrade: Breaking Changes and Migration Guide
2888
3333
 
2889
3334
  Instructions for the agent to follow when migrating a Harper application to v5, covering all breaking changes and required code updates.
2890
3335
 
@@ -3056,7 +3501,7 @@ record = { ...record, property: 'changed' };
3056
3501
  - Automatic context tracking simplifies code but requires explicit `commit()` or new `transaction()` calls when you need to observe data written within the same transaction.
3057
3502
  - Use `getContext` from `'harper'` rather than passing context manually through every call — this is the recommended pattern in v5.
3058
3503
 
3059
- ### 4.8 Delegating to the Built-in Agent
3504
+ ### 4.9 Delegating to the Built-in Agent
3060
3505
 
3061
3506
  Harper 5.2+ ships with a **built-in agent** that runs _inside_ the server, on the main thread
3062
3507
  adjacent to the operations API. Because it runs in-process, it can do things a remote client