Product docs and API reference are now on Akamai TechDocs.
Search product docs.
Search for “” in product docs.
Search API reference.
Search for “” in API reference.
Search Results
 results matching 
 results
No Results
Filters
Building a Web Application on Top of SurrealDB
Traducciones al EspañolEstamos traduciendo nuestros guías y tutoriales al Español. Es posible que usted esté viendo una traducción generada automáticamente. Estamos trabajando con traductores profesionales para verificar las traducciones de nuestro sitio web. Este proyecto es un trabajo en curso.
The SurrealDB server includes an API layer that can be schematized alongside the underlying database. This means that a go-between, server-side application is often no longer necessary. Most frontend applications can get all they need directly from SurrealDB’s robust API. SurrealDB’s approach is especially compelling with modern, adaptable architectures like Jamstack.
Learn more about SurrealDB’s APIs and building frontend application with them in this tutorial. See how to set up a SurrealDB instance and example static frontend that leverages SurrealDB’s all-in-one design.
How to Prepare SurrealDB
Before working with SurrealDB as a backend for your web application, you need to have a SurrealDB instance ready for the role. This section of the tutorial shows how to set up a basic SurrealDB instance to support an example web application.
Many of the steps here build on mechanics covered in our other SurrealDB guides linked below. Refer to those in full when you are ready to learn more and expand on the fundamentals.
Setting Up a SurrealDB Instance
The first step is to have a SurrealDB server installed and running. Additionally, the server should operate with some basic security considerations, since you intend to start using it in your application right away.
To lay this foundation, follow two of our previous guides:
Use Getting Started with SurrealDB to learn how to install and run SurrealDB.
Use Managing Security and Access Control for SurrealDB to learn about creating limited users and disabling root access.
Referencing those two guides, you need to do the following to keep up with the rest of this tutorial:
See the relevant section of the Getting Started guide linked above to install SurrealDB.
Start SurrealDB on
localhost
with an initial root user and local file storage:surreal start --bind 127.0.0.1:8000 --user root --pass exampleRootPass file:///home/example-user/.surrealdb/example.db
Access SurrealDB CLI as the root user within the namespace
application
and the databasetodo
:surreal sql --conn http://localhost:8000 --user root --pass exampleRootPass --ns application --db todo --pretty
Create a limited user with database-level access. This example names the user
exampleUser
.DEFINE LOGIN exampleUser ON DATABASE PASSWORD 'examplePass';
Close the SurrealDB CLI and stop the SurrealDB server with the Ctrl + C key combination.
Open the default port (
8000
) for the SurrealDB server in your system’s firewall:On a Debian or Ubuntu system, refer to our How to Configure a Firewall with UFW guide, and use commands like the following to open the port:
sudo ufw allow 8000/tcp sudo ufw reload
On a CentOS or similar system (e.g. AlmaLinux and Rocky Linux), refer to our Configure a Firewall with Firewalld guide, and use commands like the following to open the port:
sudo firewall-cmd --zone=public --add-port=8000/tcp --permanent sudo firewall-cmd --reload
Start the SurrealDB server using the same local file storage as before, but remove the root user and bind the server to any address:
surreal start --bind 0.0.0.0:8000 file:///home/example-user/.surrealdb/example.db
Configuring the SurrealDB Schemas
To prepare the SurrealDB database for the application, you should define the schemas that your application needs. The schemas your application needs vary widely depending on your application, and you need to plan out its features to model your databases effectively.
Since this tutorial uses an example to-do list application to demonstrate, the steps here can provide a basic model. Follow along to see how you can use SurrealDB’s DEFINE
commands to craft a database for your own application’s needs.
For more on modeling databases in SurrealDB, take a look at the SurrealDB documentation linked at the end of this guide. For more advanced modeling ideas, check out our Modeling Data with SurrealDB’s Inter-document Relations guide.
Create a file to store your SurrealQL queries. This tutorial names the file
surreal.surql
. To execute SurrealDB queries over HTTP using cURL, it is easiest to work with queries stored in a file like this.nano surreal.surql
Each set of SurrealQL commands below should be added to this file. The last step in this section then shows how to execute all of the commands together using a single cURL command.
Define a user account scope, called
account
, and give the scopeSIGNUP
andSIGNIN
functions. This single command lays the basis for user access that the example application can use for full login functionality.To learn more, take a look at the section on scoped user accounts in our Managing Security and Access Control for SurrealDB guide.
- File: surreal.surql
1 2 3
DEFINE SCOPE account SESSION 24h SIGNUP (CREATE user SET username = $username, pass = crypto::argon2::generate($pass)) SIGNIN (SELECT * FROM user WHERE username = $username AND crypto::argon2::compare(pass, $pass));
Define the
item
table for storing to-do items. The table is defined asSCHEMAFULL
, meaning that it has a defined schema (provided in the next step) that data must adhere to.The table also has a set of permissions that apply to scoped user accounts. A record can only be viewed (
select
) and modified (update
) by a user with an ID matching the record’sowner
field. A record can be created by any user account, but no user can delete a record.- File: surreal.surql
4 5 6 7 8
DEFINE TABLE item SCHEMAFULL PERMISSIONS FOR select, update WHERE owner = $token.ID FOR create WHERE $scope = "account" FOR delete NONE;
Define the fields for the
item
table. Doing so essentially sets up the table’s schema.Using
ASSERT
allows limits to be placed on a field’s possible contents.VALUE
allows the field to define default content. The$value
variable corresponds to the input value for that field.- File: surreal.surql
9 10 11 12 13 14 15 16
DEFINE FIELD description ON TABLE item TYPE string ASSERT $value != NONE; DEFINE FIELD completed ON TABLE item TYPE bool VALUE $value OR false; DEFINE FIELD owner ON TABLE item TYPE string VALUE $value OR $token.ID; DEFINE FIELD date ON TABLE item TYPE datetime VALUE time::now();
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Execute these SurrealQL commands using cURL and the limited user login created in the previous section. The example command here uses the credentials shown above:
curl -X POST -H "Accept: application/json" -H "NS: application" -H "DB: todo" --user "exampleUser:examplePass" --data-binary "@surreal.surql" http://localhost:8000/sql | jq
How to Build the Serverless Application
The SurrealDB database is now prepared to act as the backend for your application. The SurrealDB server exposes a set of HTTP APIs. Your frontend application can then leverage these APIs. In many cases, this eliminates the need for a separate backend application.
Moreover, the schema setups in the previous section allow you to work with the SurrealDB endpoints more easily. By managing the schemas’ default values and restrictions, you can implement logic that distinguishes API access.
All of this makes SurrealDB an excellent backend for Jamstack architectures, which is what the rest of this guide uses. You can learn more about Jamstack through our guide Getting Started with the Jamstack.
Specifically, the next steps in this tutorial help you set up a basic frontend application using the Gatsby framework. Gatsby uses React to generate static sites, and thus makes a good base for a Jamstack application. Learn more about Gatsby in our guide Generating Static Sites with Gatsby.
Setting Up the Prerequisites
Before developing the code for the Gatsby frontend, you need to install some prerequisites. Additionally, this tutorial generates the new Gatsby project from a base template to streamline the development.
First, follow along with our Installing and Using NVM guide to install the Node Version Manager (NVM).
Then run the commands below to install and start using the current LTS release of Node.js:
nvm install --lts nvm use --lts
Install the Gatsby command-line tool as a global NPM package:
npm install -g gatsby-cli
Generate the new Gatsby project from the default starter template, then change into the project directory. The command here creates the new project as
surreal-example-app
in the current user’s home directory:cd ~/ gatsby new surreal-example-app https://github.com/gatsbyjs/gatsby-starter-default cd surreal-example-app/
Install the SurrealDB JavaScript library to the project. While the project could interact with the SurrealDB server via HTTP, the SurrealDB library provides a much more convenient interface.
npm install surrealdb.js --save
Customize the project’s metadata as you see fit. The metadata for the Gatsby application is stored in the
gatsby-config.js
file.nano gatsby-config.js
Here is an example of the kinds of changes you might make:
- File: gatsby-config.js
11 12 13 14 15 16
siteMetadata: { title: `Example App for SurrealDB`, description: `An example serverles web application to demonstrate SurrealDB.`, author: `Example User`, siteUrl: `https://example-user.example.com`, },
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Open the default port to be used for the Gatsby application in your system’s firewall. By default, Gatsby’s development server runs on port
8000
. However, since that port is used by the SurrealDB server, this tutorial adopts port8080
for its Gatsby examples.On a Debian or Ubuntu system, use commands like the following to open the port:
sudo ufw allow 8080/tcp sudo ufw reload
On a CentOS Stream or similar system (e.g. AlmaLinux or Rocky Linux), use commands like the following to open the port:
sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent sudo firewall-cmd --reload
Now test the default Gatsby application by running the Gatsby development server:
gatsby develop -H 0.0.0.0 -p 8080
Open a web browser and navigate to port
8080
of your system’s public IP address to see the Gatsby application.When done, stop the development server with the Ctrl + C key combination.
Building the Application
With Gatsby installed and the base project set up, you can now start developing the example application itself. This mostly involves editing key parts of the default Gatsby application and adding a few components and services.
Throughout the example code that follows, comments are provided to help navigate what each part of the application does.
Defining the Display Components
Open the
src/pages/index.js
file:nano src/pages/index.js
Delete the file’s existing contents and replace it with the following:
- File: src/pages/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Import modules for building and styling the page import React from 'react'; import Layout from '../components/layout'; import * as styles from '../components/index.module.css'; // Import the custom components for rendering the page import Login from '../components/login'; import Home from '../components/home'; // Import a function from the custom authentication service import { isSignedIn } from '../services/auth'; // Render the page const IndexPage = () => { return ( <Layout> { isSignedIn() ? ( <Home /> ) : ( <Login /> ) } </Layout> ) } export default IndexPage
This defines the landing page for the Gatsby application. In this example, the page simply wraps and displays one or two components,
Home
orLogin
, depending on the login status.When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Now open the
src/components/layout.js
file:nano src/components/layout.js
Make two changes to the file:
First, add an
import
statement for aNavBar
component. Place this line alongside the otherimport
statements near the beginning of the file.- File: src/components/layout.js
10
import NavBar from './nav-bar';
Modify the
return
section to remove theHeader
component and add theNavBar
component just above themain
element:- File: src/components/layout.js
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
return ( <> <div style={{ margin: `0 auto`, maxWidth: `var(--size-content)`, padding: `var(--size-gutter)`, }} > <NavBar /> <main> {children} </main> <footer style={{ marginTop: `var(--space-5)`, fontSize: `var(--font-sm)`, }} > © {new Date().getFullYear()} · Built with {` `} <a href="https://www.gatsbyjs.com">Gatsby</a> </footer> </div> </> )
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Create a
src/components/nav-bar.js
file:nano src/components/nav-bar.js
Give the file the contents below to create the
NavBar
component, which houses the Logout option for the application:- File: src/components/nav-bar.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// Import React for rendering import React from 'react'; // Import functions from the custom authentication service import { isSignedIn, handleSignOut } from '../services/auth'; // Render the component export default function NavBar() { return ( <div style={{ display: "flex", flex: "1", margin: "1em", justifyContent: "space-between", }} > <span></span> <nav> { isSignedIn() ? ( <a href="/" onClick={ event => { handleSignOut(); window.location.reload(); } }>Logout</a> ) : ( <span></span> ) } </nav> </div> ) }
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Create a
src/components/login.js
file:nano src/components/login.js
Give it the contents here to create a
Login
component used to render the page for user logins:- File: src/components/login.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
// Import modules for rendering the component and for React's state and // effect features import React, { useState, useEffect } from 'react'; import * as styles from '../components/index.module.css'; // Import functions from the custom authentication service import { handleSignUp, handleSignIn } from '../services/auth'; const Login = () => { // Initialize the components state variables, used for the login form // submission const [usernameInput, setUsernameInput] = useState(''); const [passwordInput, setPasswordInput] = useState(''); const [isSignUp, setIsSignUp] = useState(false); // Define a function for handling user login const handleSignInSubmit = (e) => { e.preventDefault(); if (!isSignUp) { handleSignIn(usernameInput, passwordInput) .then( () => { window.location.reload(); }); } else { handleSignUp(usernameInput, passwordInput) .then( () => { window.location.reload(); }); } } // Render the component return ( <div className={styles.textCenter}> <h1> Example App for SurrealDB </h1> <p className={styles.intro}> A serverless application to demonstrate using SurrealDB as a full backend. </p> <div className={styles.textCenter}> <h3> Login </h3> <form onSubmit={handleSignInSubmit}> <div style={{ marginBottom: '2em' }}> <label> Username <input type='text' name='username' id='username' onChange={ (e) => setUsernameInput(e.target.value) } /> </label> <label> Password <input type='password' name='password' id='password' onChange={ (e) => setPasswordInput(e.target.value) } /> </label> </div> <div> <label> Sign In <input type='radio' name='signinRadio' checked onClick={ () => setIsSignUp(false) } /> </label> {' '} or {' '} <label> Sign Up <input type='radio' name='signinRadio' onClick={ () => setIsSignUp(true) } /> </label> </div> <div> <input type='submit' value='Submit' /> </div> </form> </div> </div> ) } export default Login
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Create a
src/components/home.js
file:nano src/components/home.js
Give it the contents below to create the
Home
component, rendering the to-do list for a logged-in user:- File: src/components/home.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
// Import modules for rendering the component and for React's state and // effect features import React, { useState, useEffect } from 'react'; import * as styles from '../components/index.module.css'; // Import functions for working with to-do items from the custom service import { fetchItems, submitItem, completeItem } from '../services/todo'; const Home = () => { // Initialize the state variables: // - For the loaded list of items const [itemsList, setItemsList] = useState([]); const [itemsLoaded, setItemsLoaded] = useState(false); // - For the list of items marked for completion const [completionList, setCompletionList] = useState([]); // - For any new item being entered const [newTodoItem, setNewTodoItem] = useState(''); // Define an effect for loading the list of items useEffect(() => { const fetchData = async () => { await fetchItems() .then(result => { setItemsList(result[0].result); setItemsLoaded(true); }); } fetchData(); }, []); // Define a function to keep the list of items marked for completion // updated const processCompletions = (e) => { setCompletionList(e.target.checked ? completionList.concat(e.target.id) : completionList.filter(item => item !== e.target.id)); } // Define a function for submitting items for completion const submitCompletions = async () => { for (const item of completionList) { const result = await completeItem(item); } window.location.reload(); } // Define a function for submitting a new item const addNewTodoItem = async (e) => { e.preventDefault(); if (newTodoItem !== '' && newTodoItem.length > 0) { const result = await submitItem(newTodoItem); if (result) { window.location.reload(); } else { alert('An error occurred'); } } } // Render the component return ( <div className={styles.textCenter}> <h1> To-do List </h1> <p className={styles.intro}> An example application using SurrealDB. </p> <div className={styles.textCenter} style={{ marginBottom: '2em' }}> { itemsList.map(item => ( <div key={item.id} className={styles.textCenter}> <input type='checkbox' id={item.id} checked={completionList.includes(item.id)} onChange={ (e) => processCompletions(e) } /> <span>{' ' + item.description}</span> </div> )) } <input type='button' value='Complete Selected' onClick={ () => submitCompletions() } /> </div> <div className={styles.textCenter} style={{ marginBottom: '2em' }}> <form onSubmit={addNewTodoItem}> <label> New To-do Item <input type='text' name='newTodo' id='newTodo' onChange={ (e) => setNewTodoItem(e.target.value) } /> </label> <input type='submit' value='Add Item' /> </form> </div> </div> ) } export default Home
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.
Defining the Service Interfaces
Create a
src/services
directory to store files that provide interfaces to external services. In this case, the files expose functions that leverage the SurrealDB JavaScript library to authenticate sessions and fetch and submit data.mkdir src/services
Create a
src/services/auth.js
file:nano src/services/auth.js
Give it the contents below to create a set of service functions for authenticating SurrealDB user sessions. Make sure to replace
<SurrealDB-Server-IP-Addres-Or-Domain-Name>
with your actual SurrealDB server’s IP address or domain name, if configured.- File: src/services/auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
// Import the SurrealDB library import Surreal from 'surrealdb.js'; // Define the URL for your SurrealDB service; /rpc is the server's endpoint // for WebSocke/library connections const SURREAL_URL = 'http://<SurrealDB-Server-IP-Addres-Or-Domain-Name>:8000/rpc'; // Define convenience functions for fetching and setting a userToken within // the browser's local storage, giving session persistence const getCurrentToken = () => { if (typeof window !== 'undefined' && window.localStorage.getItem('userToken')) { return window.localStorage.getItem('userToken'); } else { return null; } } const setCurrentToken = (userToken) => { window.localStorage.setItem('userToken', userToken); } // Define a function for checking whether a user is logged in by referring // to the current userToken stored in the browser export const isSignedIn = () => { const currentToken = getCurrentToken(); return (currentToken !== null && currentToken !== ''); } // Define a function for generating an authenticated database session; used // mostly by other services to eliminate repetitious log-in code export const generateAuthenticatedConnection = async () => { if (isSignedIn()) { const db = new Surreal(SURREAL_URL); await db.authenticate(getCurrentToken()); await db.use({ ns: 'application', db: 'todo' }); return db } else { return null } } // Define a set of functions for handling logins; the handleSignUp and // handleSignIn functions call the postSignIn function with a flag // indicating the type of authentication needed const postSignIn = async (username, password, isSignUp) => { if (username && password && username !== '' && password !== '') { const db = new Surreal(SURREAL_URL); const signInData = { NS: 'application', DB: 'todo', SC: 'account', username: username, pass: password } if (isSignUp) { await db.signup(signInData) .then(response => { setCurrentToken(response); db.close(); }); } else { await db.signin(signInData) .then(response => { setCurrentToken(response); db.close(); }); } } else { handleSignOut(); } } export const handleSignUp = async (username, password) => { await postSignIn(username, password, true); } export const handleSignIn = async (username, password) => { await postSignIn(username, password, false); } // Define a function to log out by removing the userToken from browser storage export const handleSignOut = () => { setCurrentToken(''); }
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.Create a
src/services/todo.js
file:nano src/services/todo.js
Give it the following contents to define a set of service functions for the application to fetch and submit changes to to-do items:
- File: src/services/todo.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
// Import the SurrealDB library import Surreal from "surrealdb.js"; // Import the authentication service for generating an database connection import { generateAuthenticatedConnection } from "../services/auth"; // Define a function for fetching items that are not marked completed; // already the database handles limiting the results to those matching // the current user's ID export const fetchItems = async () => { const db = await generateAuthenticatedConnection(); if (db) { const query = db.query('SELECT id, description, date FROM item WHERE completed = false ORDER BY date;'); return query } else { return [] } } // Define a function for submitting a new item; new items only need a // description, as the schema generates the rest of the data export const submitItem = async (itemDescription) => { const db = await generateAuthenticatedConnection(); if (db) { const query = await db.create('item', { description: itemDescription }); return query } else { return null } } // Define a function for marking an item completed export const completeItem = async (itemId) => { const db = await generateAuthenticatedConnection(); if (db) { const query = await db.merge(itemId, { completed: true }); return query } else { return null } }
When done, press CTRL+X, followed by Y then Enter to save the file and exit
nano
.
How to Run the Serverless Application with a SurrealDB Backend
Now it’s time to see the whole setup in action. With the server configured, running, and the application built, you can start the Gatsby development server to see the example application.
Follow along with the steps here to ensure that everything is in place and start using the application.
Further on you can find a suggestion for how to prepare the application for production deployment. It specifically focuses on using object storage for an efficient Jamstack deployment.
Make sure the SurrealDB server is running on the expected address and port and using the database file from before:
surreal start --bind 0.0.0.0:8000 file:///home/example-user/.surrealdb/example.db
Start the Gatsby development server again using the same address and port as when you initially tested the Gatsby project further above:
gatsby develop -H 0.0.0.0 -p 8080
Open a web browser and navigate to port
8080
on your system’s public IP address.
You should now see the login page for the example to-do list application:
Use the Sign Up option to create a user account. From there, you should be logged in and viewing the to-do list page. You can add items using the form, which should produce a list of those items, like so:
Deploying the Application to Object Storage
There are several options for deploying a Jamstack application like the one shown throughout this tutorial. With the SurrealDB server providing an accessible backend and a static site for the frontend, you have a lot of versatility for hosting.
See the deployment section of the Gatsby guide linked above for more ideas.
For a more traditional static-site deployment process, read Set up a Web Server and Host a Website on Linode. Additionally, our guide on how to Deploy a Static Site using Hugo and Object Storage showcases a modern and streamlined process using object storage.
Object storage provides an efficient and powerful possibility for hosting the static frontends of Jamstack applications.
The steps that follow outline a method for deploying the Gatsby application created in this tutorial to Linode Object Storage. These steps are stated broadly, so supplement them with the more specific instructions in the guides linked above as needed.
Build the Gatsby application. To do so, execute the command here in the project’s directory. Gatsby renders the application in a set of static files that can be served.
gatsby build
Install
s3cmd
and configure it for your Linode Object Storage credentials and settings. See how to do that in our guide Using S3cmd with Object Storage.Use
s3cmd
to create a new bucket, initialize the bucket as a website, and sync the application’s static files to the bucket:s3cmd mb s3://example-surreal-app s3cmd ws-create --ws-index=index.html --ws-error=404.html s3://example-surreal-app s3cmd --no-mime-magic --acl-public --delete-removed --delete-after sync public/ s3://example-surreal-app
At this point, you can access the application at the Linode Object Storage website URL, such as
example-surreal-app.website-[cluster-id].linodeobjects.com
.Optional: If using a custom domain name, create a
CNAME
domain record mapping the object storage bucket’s URL to the same domain name as your SurrealDB server. Doing so can help prevent CORS-related difficulties. See how to do this in the Next Steps section of the object storage deployment guide.
Conclusion
This tutorial provides the tools needed to start implementing SurrealDB as a backend for applications. Doing so can often save significant time and effort in creating and maintaining backend APIs.
More importantly, leveraging SurrealDB’s APIs can make applications more adaptable. Whether it’s for a traditional frontend, or a modern architecture like Jamstack with a static site generator, SurrealDB can be a full backend resource. This provides a lot of flexibility.
Be sure to look at our other guides on SurrealDB, linked earlier in this tutorial. Additionally, learn more about schemas and document relations with our guide on Modeling Data with SurrealDB’s Inter-document Relations.
More Information
You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.
This page was originally published on