{"result":{"depiction_url":null,"edges":{"observes":{"objects":[{"created":"2026-09-09T13:47:43Z","object_id":{"id":1437,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_identity_verification","title":"observe_identity_verification\/2","uri":"https:\/\/zotonic.com\/id\/1437"},"seq":1},{"created":"2026-09-09T13:47:43Z","object_id":{"id":1439,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_logon_ready_page","title":"observe_logon_ready_page\/2","uri":"https:\/\/zotonic.com\/id\/1439"},"seq":2},{"created":"2026-09-09T13:47:43Z","object_id":{"id":1444,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_signup","title":"observe_signup\/2","uri":"https:\/\/zotonic.com\/id\/1444"},"seq":3},{"created":"2026-09-09T13:47:43Z","object_id":{"id":1450,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_signup_url","title":"observe_signup_url\/2","uri":"https:\/\/zotonic.com\/id\/1450"},"seq":4}],"predicate":{"id":2550,"is_a":["meta","predicate"],"name":"observes","title":{"_type":"trans","tr":{"en":"Observes"}},"uri":"https:\/\/zotonic.com\/id\/observes"}},"references":{"objects":[{"created":"2020-05-30T05:48:03Z","object_id":{"id":1353,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_modules","title":{"_type":"trans","tr":{"en":"Modules"}},"uri":"https:\/\/zotonic.com\/id\/1353"},"seq":1000000}],"predicate":{"id":332,"is_a":["meta","predicate"],"name":"references","title":{"_type":"trans","tr":{"en":"References"}},"uri":"https:\/\/zotonic.com\/id\/references"}},"subject":{"objects":[{"created":"2026-09-09T13:47:43Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-09-09T13:47:43Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-09-09T13:47:43Z","object_id":{"id":2582,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_forms","title":"Forms","uri":"https:\/\/zotonic.com\/id\/2582"},"seq":3},{"created":"2026-09-09T13:47:43Z","object_id":{"id":2589,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_identity_and_accounts","title":"Identity and accounts","uri":"https:\/\/zotonic.com\/id\/2589"},"seq":4},{"created":"2026-09-09T13:47:43Z","object_id":{"id":2590,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_authentication","title":"Authentication","uri":"https:\/\/zotonic.com\/id\/2590"},"seq":5},{"created":"2026-09-09T13:47:43Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":6},{"created":"2026-09-09T13:47:43Z","object_id":{"id":2637,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_create","title":"Create","uri":"https:\/\/zotonic.com\/id\/2637"},"seq":7}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":2010,"is_a":["text","documentation","reference","module"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/2010"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_module_mod_signup","page_url":{"en":"https:\/\/zotonic.com\/docs\/2010\/mod_signup","x-default":"https:\/\/zotonic.com\/docs\/2010\/mod_signup"},"preview_url":null,"resource":{"version":3996,"pivot_location_lat":null,"title":"mod_signup","is_authoritative":true,"body":"<p>This module presents an interface for letting users register themselves.<\/p>\n<h2>Signup flow<\/h2>\n<p>There are three ways to start a signup:<\/p>\n<ul><li>Visit the public <code>signup<\/code> dispatch rule directly.<\/li><li>Ask the notification system for a <code>#signup_url{}<\/code>. This is used by modules\nthat already know some user properties or identities and want to continue on\nthe normal signup page.<\/li><li>Notify <code>#signup{}<\/code> directly. This is typically used after an external\nauthentication service has identified a visitor and the site wants to create a\nZotonic user without rendering the email signup form.<\/li><\/ul>\n<p>The <code>#signup_url{}<\/code> flow stores the caller supplied <code>props<\/code> and <code>signup_props<\/code>\nin <code>mod_server_storage<\/code>. The key is a generated check id and the generated URL\ncontains that id as the <code>xs<\/code> query argument. The stored value is\n<code>{CheckId, Timestamp, Props, SignupProps}<\/code> so the signup controller can verify\nthat the looked-up value belongs to the supplied <code>xs<\/code> and reject expired\npayloads. The stored payload is accepted for one hour after it was generated;\nafter that the signup page falls back to a normal empty signup and deletes the\nexpired server-storage entry.<\/p>\n<p><code>props<\/code> are resource properties for the person resource that will become the\nuser, for example <code>email<\/code>, <code>name_first<\/code>, <code>name_surname<\/code>, or <code>depiction_url<\/code>.\n<code>signup_props<\/code> are signup control values and identities, for example\n<code>{user_id, Id}<\/code> to update an existing person, <code>{ready_page, Url}<\/code> for the\npost-signup redirect, or <code>{identity, {Type, Key, IsUnique, IsVerified}}<\/code>.<\/p>\n<p>When <code>controller_signup<\/code> renders the page it consumes <code>xs<\/code>, fetches the stored\npayload, checks the stored timestamp, and passes the accepted values to the\ntemplates as <code>props<\/code>, <code>signup_props<\/code>, and optionally <code>email<\/code>. The templates\ninclude these values in the wired postbacks so the staged email signup can\ncontinue with the same prefilled data. If <code>xs<\/code> is missing, empty, unknown,\nexpired, or points to a different check id, the signup page falls back to a\nnormal empty signup.<\/p>\n<p>The public email signup is staged:<\/p>\n<ol><li>The visitor enters or confirms an email address. The controller checks for\nexisting accounts, blocked addresses, rate limits, and external providers.<\/li><li>For a new local account, a short one-time code is stored in the\n<code>mod_signup<\/code> gen_server under <code>{signup, EmailNorm}<\/code> and mailed with\n<code>email_signup_code.tpl<\/code>.<\/li><li>The visitor enters the code. A valid code is deleted and the final account\ndetails form is rendered.<\/li><li>The final form posts resource fields and signup identities. The controller\nrechecks username and email uniqueness, calls <code>signup_existing\/5<\/code>, logs the\nnew user on, and sends a one-time authentication token to the client auth\nmodel for the browser redirect.<\/li><\/ol>\n<p>The lower level <code>#signup{}<\/code> notification and <code>signup\/4<\/code> API skip the staged\nemail-code controller flow. They still run the signup preflight checks, insert\nor update the user resource, add identities, emit <code>#signup_done{}<\/code>, and either\nconfirm the signup immediately or leave the account unpublished and unverified\nso identity verification can be requested through <code>#identity_verification{}<\/code>.<\/p>\n<h2>Controllers<\/h2>\n<p><code>controller_signup<\/code> renders <code>signup.tpl<\/code> and handles the browser postbacks for\nthe email signup flow. It owns the short email-code confirmation, prefilled\n<code>xs<\/code> payload handling, final form validation, signup execution, logon, and\nclient-side redirect token. The page itself includes <code>_signup_box.tpl<\/code>; other\npages can include <code>_signup_box.tpl<\/code> directly to show the same signup UI without\nusing the public signup page template.<\/p>\n<p><code>controller_signup_confirm<\/code> renders <code>signup_confirm.tpl<\/code> and handles account\nidentity confirmation links. The confirmation email contains a <code>signup_confirm<\/code>\nURL with an identity verification key. The controller looks up that key,\npublishes the user resource, marks the account and identity as verified, emits\n<code>#signup_confirm{id=UserId}<\/code>, logs the user on, and redirects to\n<code>#signup_confirm_redirect{}<\/code> or the user&#39;s page.<\/p>\n<h2>Adding fields to the signup form<\/h2>\n<p>Extra person-resource fields can be added by combining template customization\nwith the <code>signup_form_fields<\/code> fold notification.<\/p>\n<p>The final email-signup form is rendered by <code>_signup_with_email_step3.tpl<\/code>. A\nsite can override that template or one of its blocks to add inputs. For every\ninput that should become a user resource property, add <code>{Field, Validate}<\/code> to\nthe <code>signup_form_fields<\/code> fold result. <code>Field<\/code> can be an atom or binary.\n<code>Validate<\/code> decides whether the controller reads the field with\n<code>z_context:get_q_validated\/2<\/code> or with <code>z_context:get_q\/2<\/code>. Values are trimmed,\nuploaded files are ignored, and values from the stored <code>props<\/code> payload take\nprecedence over posted form values.<\/p>\n<p>An observer can add fields like this:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">observe_signup_form_fields(Fields, _Context) -&gt;\n    [\n        {phone, false},\n        {address_street_1, true}\n        | Fields\n    ].\n<\/code><\/pre>\n<p>For fields that are not plain user resource properties, observe\n<code>#signup_check{}<\/code> to inspect or rewrite <code>Props<\/code> and <code>SignupProps<\/code> before the\nuser is created, or observe <code>#signup_done{}<\/code> to perform follow-up work after a\nsuccessful signup.<\/p>\n<h2>Standard pages<\/h2>\n<p>On installation, <code>mod_signup<\/code> creates two published text resources used by the\nsignup and logon templates:<\/p>\n<table class=\"table\"><thead><tr><th>Resource name<\/th><th>Default page path<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>signup_tos<\/code><\/td><td><code>\/terms<\/code><\/td><td>Terms of Service<\/td><\/tr><tr><td><code>signup_privacy<\/code><\/td><td><code>\/privacy<\/code><\/td><td>Privacy Policy<\/td><\/tr><\/tbody><\/table>\n<p>The templates resolve the links by resource name using\n<code>m.rsc.signup_tos.page_url<\/code> and <code>m.rsc.signup_privacy.page_url<\/code>. The page paths\nand contents can be changed, but the resource names must remain available for\nthe links to be shown. The installed pages contain placeholder text that should\nbe replaced with the site&#39;s own terms and privacy policy.<\/p>\n<h2>Configuration<\/h2>\n<p>You can adjust this module’s behaviour with the following <a href=\"\/id\/doc_developerguide_modules#dev-configuration-parameters\">Module configuration<\/a>:<\/p>\n<table class=\"table\"><thead><tr><th>Key<\/th><th>Default<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>mod_signup.request_confirm<\/code><\/td><td><code>true<\/code><\/td><td>Send a signup confirmation e-mail to new users. If set to <code>false<\/code>, users are verified immediately.<\/td><\/tr><tr><td><code>mod_signup.username_equals_email<\/code><\/td><td><code>true<\/code><\/td><td>If <code>true<\/code>, the user’s e-mail address is also the username (users can log in with their e-mail). If <code>false<\/code>, users can choose a separate username.<\/td><\/tr><tr><td><code>mod_signup.email_unique<\/code><\/td><td><code>false<\/code><\/td><td>If <code>true<\/code>, a signup e-mail address may not already belong to another user account. It also may not match an e-mail identity on another resource where that identity has <code>is_unique<\/code> set. E-mail identities on non-account resources do not block signup when they are not marked unique. If <code>false<\/code>, multiple users can have the same verified e-mail address.<\/td><\/tr><tr><td><code>mod_signup.member_category<\/code><\/td><td><code>person<\/code><\/td><td>Name of the category that users created through sign up will be placed in.<\/td><\/tr><tr><td><code>mod_signup.content_group<\/code><\/td><td>empty string<\/td><td>Name of the content group that users created through sign up will be placed in. The empty string means the default content group for the current ACL module.<\/td><\/tr><tr><td><code>mod_signup.depiction_as_medium<\/code><\/td><td><code>false<\/code><\/td><td>If set then any depiction URL is added as a medium record to the person who signed up. Normally the depiction is added as a separate <em>depending<\/em> image resource and connected from the person using a <code>depiction<\/code> predicate.<\/td><\/tr><\/tbody><\/table>\n<h2>Config: Using the user’s e-mail address as username<\/h2>\n<p>By setting a configuration value, it is possible to use the entered email address as the username.<\/p>\n<p>Set the configuration value <code>mod_signup.username_equals_email<\/code> to <code>true<\/code>.<\/p>\n<p>This makes the username equal to the email address, so that the user can log in using his email address instead of a\nseparate user name. Note that when you allow a user to change his email, take care to update the <code>{username_pw, {Username, Password}}<\/code> identity as well, otherwise the username remains equal to the old email address.<\/p>\n<h2>Notifications<\/h2>\n<h3><code>signup_form_fields<\/code><\/h3>\n<p>Fold for determining which signup fields to validate. This is a list of <code>{Fieldname, Validate}<\/code> tuples, defaulting to:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">[\n    {name_first, true},\n    {name_surname_prefix, false},\n    {name_surname, true}\n]\n<\/code><\/pre>\n<p>The email address is handled separately in the first signup step and is not part\nof the final form field fold.<\/p>\n<p>Observers can add \/ remove fields using the accumulator value that is passed into the notification.<\/p>\n<h3><code>#identity_verification{ user_id = UserId, identity = Ident }<\/code><\/h3>\n<p>Send verification requests to unverified identities.<\/p>\n<h3><code>#signup_check{ props = UserProps, signup_props = SignupProps }<\/code><\/h3>\n<p>Fold for the signup preflight check. Allows to add extra user properties or abort the signup.<\/p>\n<p>If no <code>{ok, _Props1, SignupProps}<\/code> is returned, but <code>{error, Reason}<\/code>, the signup is aborted.<\/p>\n<h3><code>#signup_done{ id = Id, is_verified = IsVerified, props = Props, signup_props = SignupProps }<\/code><\/h3>\n<p>Fired when a signup procedure is done and a user has been created.<\/p>\n<h3><code>#signup_confirm{ id = UserId }<\/code><\/h3>\n<p>Fired when a users have signed up and confirmed their identity (e.g. via e-mail).<\/p>\n<h3><code>#signup_confirm_redirect{ id = UserId }<\/code><\/h3>\n<p>Decide to which page a user gets redirected to after signup.\nUser signup module handling registration, activation, and signup-related policies.<\/p>\n<h2>Accepted Events<\/h2>\n<p>This module handles the following notifier callbacks:<\/p>\n<ul><li><code>observe_identity_verification<\/code>: Complete signup identity verification and continue the signup\/logon flow when allowed.<\/li><li><code>observe_logon_ready_page<\/code>: Return the url to redirect to when the user logged on, defaults to the user&#39;s personal page using <code>z_auth:is_auth<\/code>.<\/li><li><code>observe_signup<\/code>: Add a new user or an existing person as user using <code>z_ids:id<\/code>.<\/li><li><code>observe_signup_url<\/code>: Generate a link to the signup page, with additional signup properties stored as xs_props.<\/li><\/ul>","slug":"mod_signup","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"04beaa2633aec9b1b6a6e58d58c08f5a47af23b53c82d8fb6c1335e0a586738d","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":320,"is_a":["meta","category"],"name":"module","title":"Modules","uri":"https:\/\/test.zotonic.com\/id\/320"},"doc_source_path":"apps\/zotonic_mod_signup\/src\/mod_signup.erl","publication_start":"2023-06-24T08:17:45Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_signup\/src\/mod_signup.erl","pivot_location_lng":null,"doc_source_kind":"module","name":"doc_module_mod_signup","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2020-05-30T05:47:50Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_signup","publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"b8c4b2ccdc223257f311968fbd17b2d66f0c4156\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-09-30T09:39:37Z","title_slug":"mod_signup"},"uri":"https:\/\/zotonic.com\/id\/2010","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/2010"}},"status":"ok"}