diff --git a/src/libraries/utils/markdown.ml b/src/libraries/utils/markdown.ml
index 3b609f20f2086bf309d278e3c4cc2600228557ef..9ccffa06409a9879d8f6e88bb3b5d614fdb31bdc 100644
--- a/src/libraries/utils/markdown.ml
+++ b/src/libraries/utils/markdown.ml
@@ -137,7 +137,7 @@ let enum items = [OL items]
 let description items = [DL items]
 
 let par text = [Block [Text text]]
-let block b = [Block b]
+let block b = if b = [] then [] else [Block b]
 
 (* -------------------------------------------------------------------------- *)
 (* --- Sectioning                                                         --- *)
diff --git a/src/plugins/server/package.ml b/src/plugins/server/package.ml
index acab9d15cab3b8ccca7abbbd9594db0ecc1abcce..f1582415df7a011ebdc3e22849f7d5ae11b665a4 100644
--- a/src/plugins/server/package.ml
+++ b/src/plugins/server/package.ml
@@ -22,48 +22,39 @@
 
 (* -------------------------------------------------------------------------- *)
 
+module Senv = Server_parameters
 module Md = Markdown
 
 (* -------------------------------------------------------------------------- *)
 
 type plugin = Kernel | Plugin of string
-type package = { plugin: plugin; pkgname: string list }
-type ident = { package: package; name: string }
-type name = string list
+type path = string list
+type ident = { plugin: plugin; package: path; name: string }
 
 let pp_plugin fmt = function
   | Kernel -> Format.pp_print_string fmt "Kernel"
   | Plugin p -> Format.fprintf fmt "Plugin %s" p
 
-let pp_name fmt = function
-  | [] -> ()
-  | p::ps ->
-    Format.pp_print_string fmt p ;
-    List.iter (fun p ->
-        Format.pp_print_char fmt '.' ;
-        Format.pp_print_string fmt p ;
-      ) ps
+let pp_step fmt a =
+  ( Format.pp_print_string fmt a ; Format.pp_print_char fmt '.' )
 
-let pp_package fmt { plugin ; pkgname } =
-  match plugin with
-  | Kernel -> pp_name fmt pkgname
-  | Plugin p -> Format.fprintf fmt "%s.%a" p pp_name pkgname
+let pp_plugin_step fmt = function
+  | Kernel -> ()
+  | Plugin p -> pp_step fmt p
 
-let pp_ident fmt { package = pkg ; name = id } =
-  Format.fprintf fmt "%a.%s" pp_package pkg id
+let pp_ident fmt { plugin ; package ; name } =
+  ( pp_plugin_step fmt plugin ;
+    List.iter (pp_step fmt) package ;
+    Format.pp_print_string fmt name )
 
 (* -------------------------------------------------------------------------- *)
 (* --- Name Resolution                                                    --- *)
 (* -------------------------------------------------------------------------- *)
 
 module Std = Transitioning.Stdlib
-
-module PkgMap =
-  Map.Make(struct type t = package let compare = Std.compare end)
-
-module IdMap =
-  Map.Make(struct type t = ident let compare = Std.compare end)
-
+module Id = struct type t = ident let compare = Std.compare end
+module IdMap = Map.Make(Id)
+module IdSet = Set.Make(Id)
 module NameSet = Set.Make(String)
 
 module Scope =
@@ -86,20 +77,21 @@ struct
     | Plugin p -> "plugin" :: p :: ids
 
   (* propose various abbreviations ; finally render full qualified name *)
-  let ranked_name source { package = pkg ; name = id } k =
-    let name = [id] in
+  let ranked source { plugin ; package ; name } k =
+    String.concat "_" @@
+    let name = [name] in
     match k with
     | 0 -> name
-    | 1 -> relative ~source ~target:pkg.plugin name
-    | 2 -> relative ~source ~target:pkg.plugin (inpkg name pkg.pkgname)
-    | 3 -> relative ~source ~target:pkg.plugin (pkg.pkgname @ name)
-    | _ -> target pkg.plugin (pkg.pkgname @ name)
+    | 1 -> relative ~source ~target:plugin name
+    | 2 -> relative ~source ~target:plugin (inpkg name package)
+    | 3 -> relative ~source ~target:plugin (package @ name)
+    | _ -> target plugin (package @ name)
 
   type t = {
     source : plugin ;
     mutable clashes : bool ;
-    mutable index : (name,(ident * int) list) Hashtbl.t ;
-    mutable names : name IdMap.t ;
+    mutable index : (string,(ident * int) list) Hashtbl.t ;
+    mutable names : string IdMap.t ;
     mutable reserved : NameSet.t ;
   }
 
@@ -112,10 +104,10 @@ struct
   }
 
   let rec non_reserved scope id rk =
-    match ranked_name scope.source id rk with
-    | [a] when NameSet.mem a scope.reserved ->
+    let a = ranked scope.source id rk in
+    if NameSet.mem a scope.reserved then
       non_reserved scope id (succ rk)
-    | ns -> ns , rk
+    else a,rk
 
   let push scope id rk =
     begin
@@ -153,10 +145,6 @@ struct
         resolve scope
       end
 
-  let name_of ns id =
-    try String.concat "." (IdMap.find id ns)
-    with Not_found -> "?"
-
 end
 
 (* -------------------------------------------------------------------------- *)
@@ -208,13 +196,14 @@ type declKindInfo =
 
 type declInfo = {
   d_ident : ident;
-  d_kind : declKindInfo;
-  d_title : Markdown.text;
   d_descr : Markdown.block;
+  d_kind : declKindInfo;
 }
 
 type packageInfo = {
-  d_package : package;
+  d_plugin : plugin ;
+  d_package : string list ;
+  d_userdoc : Markdown.elements ;
   d_content : declInfo list;
 }
 
@@ -254,42 +243,95 @@ let visit_package_used f { d_content } =
   List.iter (visit_decl f) d_content
 
 let package_resolve ?(keywords=[]) pkg =
-  let scope = Scope.create pkg.d_package.plugin in
+  let scope = Scope.create pkg.d_plugin in
   List.iter (Scope.reserve_name scope) keywords ;
   visit_package_def (Scope.reserve_ident scope) pkg ;
   visit_package_used (Scope.use scope) pkg ;
-  IdMap.map (String.concat "_") (Scope.resolve scope)
+  Scope.resolve scope
 
 (* -------------------------------------------------------------------------- *)
 (* --- Server API                                                         --- *)
 (* -------------------------------------------------------------------------- *)
 
-let ident_re = Str.regexp "^\\([a-z0-9]+\\.\\)*[a-zA-Z0-9]+$"
+type package = {
+  pkgInfo : packageInfo ; (* with empty decl *)
+  mutable revDecl : declInfo list ; (* in reverse order *)
+}
 
-let identFor ?plugin name =
-  if not (Str.string_match ident_re name 0) then
-    failwith
-      (Printf.sprintf "Invalid identifier %S (use \"abc.def.fooBar\")" name) ;
-  let plugin = match plugin with None -> Kernel | Some p -> Plugin p in
-  let path = String.split_on_char '.' name in
-  let pkgname , name = match List.rev path with
-    | [] -> failwith (Printf.sprintf "Inconsistent name %S" name)
-    | a :: ps -> List.map String.lowercase_ascii (List.rev ps) , a
-  in { package = { plugin ; pkgname } ; name }
+let registry = ref IdSet.empty (* including packages *)
+let packages = ref [] (* in reverse order *)
+
+let name_re = Str.regexp "^[a-zA-Z0-9]+$"
+let package_re = Str.regexp "^[a-z0-9]+\\(\\.[a-z0-9]+\\)*$"
+
+let check_name name =
+  if not (Str.string_match name_re name 0) then
+    Senv.fatal
+      "Invalid identifier %S (use « camlCased » names)" name
+
+let check_package pkg =
+  if not (Str.string_match package_re pkg 0) then
+    Senv.fatal
+      "Invalid package identifier %S (use dot separated lowercase names)"
+      pkg
+
+let register_ident id =
+  if IdSet.mem id !registry then
+    Senv.fatal "Duplicate identifier '%a'" pp_ident id ;
+  registry := IdSet.add id !registry
+
+let userdoc ~plugin ~title ?(descr=[]) = function
+  | None -> Md.section ~title (Md.block descr)
+  | Some readme ->
+    let file =
+      match plugin with
+      | Kernel ->
+        Printf.sprintf "%s/server/kernel/%s" Fc_config.datadir readme
+      | Plugin name ->
+        Printf.sprintf "%s/%s/server/%s" Fc_config.datadir name readme
+    in
+    if Sys.file_exists file
+    then Markdown.rawfile file
+    else Markdown.(section ~title (Md.block descr))
 
-let packages = ref PkgMap.empty
+(* -------------------------------------------------------------------------- *)
+(* --- Declarations                                                       --- *)
+(* -------------------------------------------------------------------------- *)
 
-let declare ?plugin ~id ~title ?(descr=[]) decl =
-  let ident = identFor ?plugin id in
-  let pkg = ident.package in
-  let decl = { d_ident=ident ; d_title=title ; d_descr=descr ; d_kind=decl } in
-  let content = try PkgMap.find pkg !packages with Not_found -> [] in
-  packages := PkgMap.add pkg (decl::content) !packages ; ident
+let package ?plugin ?descr ?readme ~name () =
+  check_package name ;
+  let plugin = match plugin with None -> Kernel | Some p -> Plugin p in
+  let pkgname = String.split_on_char '.' name in
+  let pkgid = { plugin ; package = pkgname ; name = "*"} in
+  let title = Printf.sprintf "Package %s" name in
+  let userdoc = userdoc ~plugin ~title ?descr readme in
+  let pkgInfo = {
+    d_plugin = plugin ;
+    d_package = pkgname ;
+    d_userdoc = userdoc ;
+    d_content = [] ;
+  } in
+  let package = { pkgInfo ; revDecl=[] } in
+  register_ident pkgid ;
+  packages := package :: !packages ;
+  package
+
+let declare ~package:pkg ~name ?(descr=[]) decl =
+  check_name name ;
+  let { d_plugin = plugin ; d_package = package } = pkg.pkgInfo in
+  let ident = { plugin ; package ; name } in
+  let decl = { d_ident=ident ; d_descr=descr ; d_kind=decl } in
+  register_ident ident ;
+  pkg.revDecl <- decl :: pkg.revDecl
 
 let iter f =
-  PkgMap.iter
-    (fun d_package d_content -> f { d_package ; d_content })
-    !packages
+  begin
+    List.iter f @@
+    List.sort (fun a b -> Std.compare a.d_plugin b.d_plugin) @@
+    List.rev_map
+      (fun pkg -> { pkg.pkgInfo with d_content = List.rev pkg.revDecl })
+      !packages
+  end
 
 (* -------------------------------------------------------------------------- *)
 (* --- JSON To MarkDown                                                   --- *)
@@ -344,7 +386,7 @@ let pp_jtype fmt js =
   let ns = Scope.resolve scope in
   let self = Md.emph "self" in
   let kind id = Md.code (Printf.sprintf "#%s" id) in
-  let data id = Md.emph (Scope.name_of ns id) in
+  let data id = Md.emph (IdMap.find id ns) in
   Markdown.pp_text fmt (md_jtype { kind ; data ; self } js)
 
 (* -------------------------------------------------------------------------- *)
diff --git a/src/plugins/server/package.mli b/src/plugins/server/package.mli
index 818f9c775be8fcaeb4ab937808226c365cf0d084..9aff800924d366035abdafee19f1c9c0e779f59d 100644
--- a/src/plugins/server/package.mli
+++ b/src/plugins/server/package.mli
@@ -25,9 +25,8 @@
 (* -------------------------------------------------------------------------- *)
 
 type plugin = Kernel | Plugin of string
-type package = private { plugin: plugin; pkgname: string list }
-type ident = private { package: package; name: string }
-type name = string list
+type path = string list
+type ident = private { plugin: plugin; package: path; name: string; }
 
 type jtype =
   | Jany
@@ -70,13 +69,14 @@ type declKindInfo =
 
 type declInfo = {
   d_ident : ident;
-  d_kind : declKindInfo;
-  d_title : Markdown.text;
   d_descr : Markdown.block;
+  d_kind : declKindInfo;
 }
 
 type packageInfo = {
-  d_package : package;
+  d_plugin : plugin;
+  d_package : path;
+  d_userdoc : Markdown.elements;
   d_content : declInfo list;
 }
 
@@ -85,16 +85,13 @@ type packageInfo = {
 (* -------------------------------------------------------------------------- *)
 
 val pp_plugin : Format.formatter -> plugin -> unit
-val pp_package : Format.formatter -> package -> unit
 val pp_ident : Format.formatter -> ident -> unit
-val pp_name : Format.formatter -> name -> unit
 val pp_jtype : Format.formatter -> jtype -> unit
 
 (* -------------------------------------------------------------------------- *)
 (* --- Imports Resolution                                                 --- *)
 (* -------------------------------------------------------------------------- *)
 
-module PkgMap : Map.S with type key = package
 module IdMap : Map.S with type key = ident
 
 module Scope :
@@ -103,8 +100,7 @@ sig
   val create : plugin -> t
   val reserve_name : t -> string -> unit (** Must _not_ be call after [use] *)
   val reserve_ident : t -> ident -> unit (** Must _not_ be call after [use] *)
-  val resolve : t -> name IdMap.t
-  val name_of : name IdMap.t -> ident -> string
+  val resolve : t -> string IdMap.t
   val use : t -> ident -> unit
 end
 
@@ -121,18 +117,26 @@ val visit_package_used: (ident -> unit) -> packageInfo -> unit
 (* --- Server API                                                         --- *)
 (* -------------------------------------------------------------------------- *)
 
+type package
+
+val package :
+  ?plugin:string ->
+  ?descr:Markdown.block ->
+  ?readme:string ->
+  name:string ->
+  unit -> package
+
 (**
    Register the declaration in the Server API.
    This is only way to obtain identifiers.
    This ensures identifiers are declared before being used.
 *)
 val declare :
-  ?plugin:string ->
-  id:string ->
-  title:Markdown.text ->
+  package:package ->
+  name:string ->
   ?descr:Markdown.block ->
   declKindInfo ->
-  ident
+  unit
 
 val iter : (packageInfo -> unit) -> unit