ZKRINET Docs
Building blocks

Forms

On Zkrinet: add the block under Building blocks in the panel — one click, your own code is untouched and a snapshot is taken first. The installation steps below apply if you run PocketBase yourself, for example after an export.

Contact and lead forms for landing pages. The visitor submits without logging in, the submission is saved in the database and emailed to you. With bot protection (honeypot field + minimum fill-in time always on, captcha optional), rate limit per IP and protection against open redirect.

Requires the Mejl (email) plugin. Tested against PocketBase 0.40.4.

Installation

  1. Install Mejl first (and set up SMTP), then copy pb_hooks/ and pb_migrations/ to your PocketBase and restart.
  2. Create a form in zk_formular in the admin panel:
Field
namnUsed in the address: kontakt → /api/zk/formular/kontakt (a–z, 0–9, -, _)
rubrikShown in the email to you, e.g. "Offertförfrågan" (quote request)
faltWhich fields the form has (see below)
mottagareWhere the submissions are emailed
aktivOff = the form responds 404
tack_urlOptional. Where regular forms are sent after submission. Must be on the app's address or in tillatna_adresser.
captchaingen, friendly, turnstile or hcaptcha
kvittoSend a short "thanks, we have received your message" to the sender (the first field of type epost)
max_per_timmeSubmissions per IP and hour. Empty = 10

falt is a JSON list:

[
  { "namn": "namn",       "etikett": "Namn",       "typ": "text",  "obligatorisk": true, "max": 100 },
  { "namn": "epost",      "etikett": "E-post",     "typ": "epost", "obligatorisk": true },
  { "namn": "telefon",    "etikett": "Telefon",    "typ": "text" },
  { "namn": "meddelande", "etikett": "Meddelande", "typ": "lang",  "obligatorisk": true, "max": 2000 }
]

Types: text (one line), lang (multiple lines), epost (email). max is the number of characters (default 200 / 5000 / 254, at most 20,000). At most 30 fields. Field names must not start with zk_. The schema is checked when you save the form. Fields that are not in the schema are never saved, regardless of what is sent.

  1. Settings: zk_formular_installningar already has one row. It holds the captcha keys (see below), tillatna_adresser (comma-separated addresses for your landing page if it is on a different domain than PocketBase, e.g. https://min-sajt.se, https://www.min-sajt.se), minsta_sekunder (default 3) and hemlighet (created automatically — do not touch it).
  1. Is PocketBase behind a proxy (Cloudflare, nginx, Caddy)? Set Settings → Trusted proxy so that PocketBase sees the visitor's real IP. Otherwise all visitors share the proxy's IP — and thereby the same limit per hour.

Usage

Every form needs a fresh start time (zk_start) from the server. It proves how long the form has been open; bots that submit immediately are rejected.

With JavaScript (fetch)

const PB = "https://din-pb.se"
const f = await fetch(`${PB}/api/zk/formular/kontakt`).then(r => r.json())
// f = { namn, rubrik, falt: [...], captcha: "friendly", sitekey: "…", start: "1762…", honungsfalt: "zk_webbplats" }

const r = await fetch(`${PB}/api/zk/formular/kontakt`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ namn: "Anna", epost: "anna@exempel.se", meddelande: "Hej!",
                         zk_start: f.start, zk_webbplats: "",
                         "frc-captcha-response": tokenFranWidgeten }),
})
// 200 { ok: true } · 400 { fel, falt: { epost: "ogiltig e-postadress" } } · 404 · 429 · 503

Regular HTML form

<form id="kontakt" method="post" action="https://din-pb.se/api/zk/formular/kontakt">
  <input name="namn" required>
  <input name="epost" type="email" required>
  <textarea name="meddelande" required></textarea>
  <!-- Honeypot field: must be empty. Hide it with CSS, NOT type="hidden" (bots skip those). -->
  <div style="position:absolute;left:-9999px" aria-hidden="true">
    <label>Leave empty <input name="zk_webbplats" tabindex="-1" autocomplete="off"></label>
  </div>
  <input type="hidden" name="zk_start">
  <input type="hidden" name="zk_tack" value="https://min-sajt.se/tack">  <!-- optional, must be allowlisted -->
  <button>Send</button>
</form>
<script>
  fetch("https://din-pb.se/api/zk/formular/kontakt").then(r => r.json())
    .then(f => { document.querySelector("#kontakt [name=zk_start]").value = f.start })
</script>

After submission the visitor is sent (303) to zk_tack or the form's tack_url. If there is none, a simple thank-you page is shown. Errors are shown as a simple error page.

Both application/x-www-form-urlencoded and multipart/form-data work. Values are saved exactly as they were typed (0701… keeps the zero).

Captcha

Choose per form. The key is stored in a hidden field in zk_formular_installningar or as an environment variable (the environment wins):

ProviderSettingEnvironment variableWidget's field
Friendly Captcha (Germany, EU — first choice)friendly_api_nyckel (hidden), friendly_sitekey, friendly_euZK_FORMULAR_FRIENDLY_NYCKELfrc-captcha-response
Cloudflare Turnstileturnstile_hemlig (hidden)ZK_FORMULAR_TURNSTILE_HEMLIGcf-turnstile-response
hCaptchahcaptcha_hemlig (hidden), hcaptcha_sitekeyZK_FORMULAR_HCAPTCHA_HEMLIGh-captcha-response

The widget adds its own field to the form. With fetch you can also send the token as zk_captcha.

Security

Limitations

Testing

./bank.py formular

21 tests. Captcha is tested against a fake provider (ZK_FORMULAR_FRIENDLY_URL, ZK_FORMULAR_TURNSTILE_URL, ZK_FORMULAR_HCAPTCHA_URL redirect the verify address; ZK_FORMULAR_CAPTCHA_TIMEOUT lowers the timeout) — nothing reaches the internet. Passed, failed, timeout and server error are tested for Friendly Captcha and Turnstile, passed/failed for hCaptcha. The plugin has not been run against the real providers. The security tests are mutant-verified: every protection has been deliberately broken and caught by a test.