Adding sign-in

RTEdge.net signs visitors in for you. Link to ::si, read the signed-in user with fetch('::su') or request.rt.user, and you write no authentication code. Visitors sign in with GitHub, Google, Microsoft or Apple. This guide adds sign-in and sign-out buttons, reads the user in a page and in a worker, and shows how to sign in without leaving the page.

Note: Sign-in suffixes work on deployed RTEdge.net hosts. The playground emulates only ::su and request.rt.user, using the account you are signed in with in the editor.

How sign-in works

Four suffixes drive sign-in. Append one to any URL of a deployed host. The platform answers them before your files or your worker are consulted.

SuffixWhat it doesReturns
::siSigns the visitor in.A small page that takes the browser to the RTCode.io sign-in page.
::saSigns in as another account. The provider picker always appears.The same page, with the picker forced.
::suReads the signed-in user.The email as a JSON string, or null.
::soSigns the visitor out of this host.A 303 redirect back to the page.

After ::si, the visitor picks GitHub, Google, Microsoft or Apple on the RTCode.io sign-in page. The browser then returns to the URL it started from, without the suffix and with its query and hash, and the host holds a session for the visitor. If the visitor is already signed in to RTCode.io, the return needs no provider interaction. See ::si ::sa ::su ::so for every detail.

One session per host

The session is signed with ECDSA P-384, checked at the edge on every request and honored for 24 hours. It belongs to the host: signing in on a.rt.ht does not sign the visitor in on b.rt.ht, and ::so signs out of the current host only. The path routes of one host share the session.

Providers and multi-factor sign-in

Visitors sign in with GitHub, Google, Microsoft or Apple. Multi-factor sign-in and passkeys work through the identity provider: enable them on the account there.

GitHub sign-in needs an email address on the GitHub account. Without one, the editor shows "Required email address is missing." with a button that opens GitHub's profile settings.

Add sign-in and sign-out buttons

Appending to location.pathname keeps the current path, query and hash, so each action returns the visitor to the same page.

Three buttons

html
<button id=signin>Sign in</button>
<button id=signas>Switch account</button>
<button id=signout>Sign out</button>
<script type=module>
  signin.onclick = () => location.pathname += '::si'
  signas.onclick = () => location.pathname += '::sa'
  signout.onclick = () => location.pathname += '::so'
</script>

Each button sends the browser to the same page with a sign-in suffix appended.

Note: These suffixes work on deployed hosts. The playground does not emulate ::si, ::sa or ::so.

Warning: A plain relative link (href=::si) resolves against the current directory. It drops the last path segment, the query and the hash: /docs/page?x=1#y becomes /docs/::si. Use it only on directory-style URLs without a query.

Read the signed-in user

In a page, fetch('::su') returns the signed-in email as a JSON string, or null. In an edge worker, request.rt.user has the same value.

WhereHowResult
A pageawait fetch('::su').then(r => r.json())A string, or null.
An edge workerrequest.rt.userA string, or null.

null means that there is no valid session on this host. The path in front of ::su does not matter, because identity is per host. ::sud Experimental also returns the provider the visitor signed in with.

Show the user in a page

html
<output id=who></output>
<script type=module>
  const user = await fetch('::su').then(r => r.json()) // string email or null
  who.value = user ?? 'signed out'
</script>

The same code works in the playground, where ::su answers with the account you are signed in with in the editor, and on a deployment, where it answers with the visitor's session on that host.

Experimental The popup forms of the suffixes, ::sic, ::sac, ::sitc, ::soc and ::suc, run the action in a popup window. The popup posts { rt: { user } } to its same-origin opener with postMessage and closes, so your page never navigates. ::suc posts the current user at once, and ::soc signs out and posts null.

html
<button id=login>Sign in</button>
<output id=me></output>
<script type=module>
  const show = user => me.value = user ?? 'signed out'
  addEventListener('message', e => { if (e.origin === location.origin && e.data?.rt) show(e.data.rt.user) })
  login.onclick = () => open(location.pathname + '::sic') // popup signs in, posts { rt: { user } }, closes
  show(await fetch('::su').then(r => r.json()))
</script>

If the visitor is already signed in to RTCode.io, the popup returns at once. If the visitor closes it before finishing, nothing is posted. The opener must be on the same origin as the popup. This works on deployed hosts only.

Protect a worker API

Worker calls with /: are not covered by edge:user, so a worker that needs a signed-in user checks request.rt.user itself and answers 401 when it is null.

Answer only signed-in visitors

html
<script type=worker edge>
  export default {
    fetch: request => request.rt.user
      ? Response.json({ hello: request.rt.user })
      : new Response('Sign in', { status: 401 })
  }
</script>

In the playground the same code sees the account you are signed in with in the editor.

Store data per user

html
<script type=worker edge>
  export default {
    fetch: async (request, { KV }) => {
      const { user, route } = request.rt
      if (!user) return Response.json({ error: 'Sign in first', signIn: '::si' }, { status: 401 })
      const kv = KV.open({ user, route }) // call synchronously, never await the namespace
      const visits = (await kv.get('visits', 'json') ?? 0) + 1
      await kv.put('visits', JSON.stringify(visits), { metadata: { at: Date.now() } })
      return Response.json({ user, visits })
    }
  }
</script>

Check request.rt.user before you open per-user storage. Without the if (!user) guard, signed-out visitors would share the route's own scope, because a falsy user opens the owner scope. See Storing data in KV.

Sign in inside a cross-site embed

Experimental ::sh is an ES module for pages that run inside an iframe on another site. Importing it adds a hidden helper frame to your page, and after sign-in the session is stored from inside the embed. register() returns an object with open('i'), open('a') and open('o') to sign in, sign in as another account and sign out. See the example on the ::si ::sa ::su ::so page.

Keep ::s out of your own paths

Warning: Any path that contains ::s is answered by the sign-in handler and never reaches your files or your worker. The action is chosen from the letters after the last ::s: /x::sorted signs the visitor out, /x::summary returns the user, and unknown letters return a 501 page. Keep ::s out of route, file and worker paths.

Sign-in in the playground

The playground emulates only part of the sign-in surface:

Both emulated answers come from the editor hook sync.requestRT, which by default adds the signed-in account's email as user. See sync. The editor consults it for each worker call and each ::su lookup, and it must answer within 5 seconds. If it does not, or if the editor is disconnected, user is null.

Troubleshooting

::su returns null after signing in

The session belongs to the host that the visitor signed in on, and lasts 24 hours. Check that the page and the ::su request use the same host, then sign in again.

A path returns a 501 page

The path contains ::s followed by letters that are not a sign-in action. Rename the route, file or worker path.

"Required email address is missing."

The GitHub account has no email address that GitHub reports. Add one in GitHub's profile settings, or sign in with another provider.

"Session has expired! Please sign in again!"

The RTCode.io sign-in has expired. Sign in again in the editor.

The popup posts nothing

The visitor closed it before finishing, or the opener is on another origin. The popup posts only to a same-origin opener.

See also